refactor: 重命名 backend-single 目录为 server
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.achievement.*;
|
||||
import com.emotion.dto.response.achievement.AchievementResponse;
|
||||
import com.emotion.entity.Achievement;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 成就服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface AchievementService extends IService<Achievement> {
|
||||
|
||||
/**
|
||||
* 分页查询成就
|
||||
*/
|
||||
IPage<Achievement> getPage(AchievementPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据分类查询成就
|
||||
*/
|
||||
List<Achievement> getByCategory(String category);
|
||||
|
||||
/**
|
||||
* 根据稀有度查询成就
|
||||
*/
|
||||
List<Achievement> getByRarity(String rarity);
|
||||
|
||||
/**
|
||||
* 根据条件类型查询成就
|
||||
*/
|
||||
List<Achievement> getByConditionType(String conditionType);
|
||||
|
||||
/**
|
||||
* 查询已解锁的成就
|
||||
*/
|
||||
List<Achievement> getUnlockedAchievements();
|
||||
|
||||
/**
|
||||
* 查询未解锁的成就
|
||||
*/
|
||||
List<Achievement> getLockedAchievements();
|
||||
|
||||
/**
|
||||
* 查询隐藏的成就
|
||||
*/
|
||||
List<Achievement> getHiddenAchievements();
|
||||
|
||||
/**
|
||||
* 查询可见的成就
|
||||
*/
|
||||
List<Achievement> getVisibleAchievements();
|
||||
|
||||
/**
|
||||
* 根据进度范围查询成就
|
||||
*/
|
||||
List<Achievement> getByProgressRange(Double minProgress, Double maxProgress);
|
||||
|
||||
/**
|
||||
* 根据解锁时间范围查询成就
|
||||
*/
|
||||
List<Achievement> getByUnlockTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计已解锁成就数量
|
||||
*/
|
||||
Long countUnlockedAchievements();
|
||||
|
||||
/**
|
||||
* 统计未解锁成就数量
|
||||
*/
|
||||
Long countLockedAchievements();
|
||||
|
||||
/**
|
||||
* 统计指定分类的成就数量
|
||||
*/
|
||||
Long countByCategory(String category);
|
||||
|
||||
/**
|
||||
* 统计指定稀有度的成就数量
|
||||
*/
|
||||
Long countByRarity(String rarity);
|
||||
|
||||
/**
|
||||
* 获取平均进度
|
||||
*/
|
||||
Double getAvgProgress();
|
||||
|
||||
/**
|
||||
* 根据分类获取平均进度
|
||||
*/
|
||||
Double getAvgProgressByCategory(String category);
|
||||
|
||||
/**
|
||||
* 获取最近解锁的成就
|
||||
*/
|
||||
List<Achievement> getRecentlyUnlocked(Integer limit);
|
||||
|
||||
/**
|
||||
* 获取接近完成的成就
|
||||
*/
|
||||
List<Achievement> getNearCompletion();
|
||||
|
||||
/**
|
||||
* 获取稀有成就
|
||||
*/
|
||||
List<Achievement> getRareAchievements();
|
||||
|
||||
/**
|
||||
* 解锁成就
|
||||
*/
|
||||
boolean unlockAchievement(String id, LocalDateTime unlockedTime);
|
||||
|
||||
/**
|
||||
* 更新成就进度
|
||||
*/
|
||||
boolean updateProgress(String id, Double progress);
|
||||
|
||||
/**
|
||||
* 更新隐藏状态
|
||||
*/
|
||||
boolean updateHiddenStatus(String id, Integer isHidden);
|
||||
|
||||
/**
|
||||
* 获取推荐成就
|
||||
*/
|
||||
List<Achievement> getRecommendedAchievements(String category, String rarity, Integer limit);
|
||||
|
||||
// 新增的Response相关方法
|
||||
|
||||
/**
|
||||
* 分页查询成就响应
|
||||
*/
|
||||
PageResult<AchievementResponse> getPageWithResponse(AchievementPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取成就响应
|
||||
*/
|
||||
AchievementResponse getAchievementResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建成就并返回响应
|
||||
*/
|
||||
AchievementResponse createAchievementWithResponse(AchievementCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新成就并返回响应
|
||||
*/
|
||||
AchievementResponse updateAchievementWithResponse(AchievementUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 根据分类查询成就响应
|
||||
*/
|
||||
List<AchievementResponse> getByCategoryWithResponse(String category);
|
||||
|
||||
/**
|
||||
* 根据稀有度查询成就响应
|
||||
*/
|
||||
List<AchievementResponse> getByRarityWithResponse(String rarity);
|
||||
|
||||
/**
|
||||
* 查询已解锁的成就响应
|
||||
*/
|
||||
List<AchievementResponse> getUnlockedAchievementsWithResponse();
|
||||
|
||||
/**
|
||||
* 查询未解锁的成就响应
|
||||
*/
|
||||
List<AchievementResponse> getLockedAchievementsWithResponse();
|
||||
|
||||
/**
|
||||
* 查询最近解锁的成就响应
|
||||
*/
|
||||
List<AchievementResponse> getRecentlyUnlockedWithResponse(Integer limit);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.AdminChangePasswordRequest;
|
||||
import com.emotion.dto.request.AdminLoginRequest;
|
||||
import com.emotion.dto.response.AdminAuthResponse;
|
||||
import com.emotion.dto.response.AdminInfoResponse;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 管理员认证服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
public interface AdminAuthService {
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @return 认证响应
|
||||
*/
|
||||
AdminAuthResponse login(AdminLoginRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前管理员信息
|
||||
*
|
||||
* @param adminId 管理员ID
|
||||
* @return 管理员信息响应
|
||||
*/
|
||||
AdminInfoResponse getCurrentAdminInfo(String adminId);
|
||||
|
||||
/**
|
||||
* 管理员登出
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 是否登出成功
|
||||
*/
|
||||
boolean logout(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新的认证响应
|
||||
*/
|
||||
AdminAuthResponse refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 是否有效
|
||||
*/
|
||||
boolean validateToken(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 从令牌中获取管理员ID
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 管理员ID
|
||||
*/
|
||||
String getAdminIdFromToken(String token);
|
||||
|
||||
/**
|
||||
* 修改管理员密码(需要原密码验证)
|
||||
*
|
||||
* @param adminId 管理员ID
|
||||
* @param request 修改密码请求
|
||||
*/
|
||||
void changePassword(String adminId, AdminChangePasswordRequest request);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.AdminCreateRequest;
|
||||
import com.emotion.dto.request.AdminPageRequest;
|
||||
import com.emotion.dto.request.AdminUpdateRequest;
|
||||
import com.emotion.dto.response.AdminResponse;
|
||||
import com.emotion.entity.Admin;
|
||||
|
||||
/**
|
||||
* 管理员服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
public interface AdminService extends IService<Admin> {
|
||||
|
||||
/**
|
||||
* 分页查询管理员
|
||||
*/
|
||||
PageResult<AdminResponse> getPageWithResponse(AdminPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取管理员响应
|
||||
*/
|
||||
AdminResponse getAdminResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建管理员并返回响应
|
||||
*/
|
||||
AdminResponse createAdminWithResponse(AdminCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新管理员并返回响应
|
||||
*/
|
||||
AdminResponse updateAdminWithResponse(AdminUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 根据账号查询管理员
|
||||
*/
|
||||
Admin getByAccount(String account);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询管理员
|
||||
*/
|
||||
Admin getByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据手机号查询管理员
|
||||
*/
|
||||
Admin getByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 重置指定管理员的密码
|
||||
*/
|
||||
void resetPassword(String adminId, String newPassword);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.AiCallLog;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.ai.AiCallLogQueryRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AiCallLogService extends IService<AiCallLog> {
|
||||
|
||||
List<AiCallLog> latest(Integer limit);
|
||||
|
||||
PageResult<AiCallLog> query(AiCallLogQueryRequest request);
|
||||
|
||||
AiCallLog findByRequestId(String requestId, String userId);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.*;
|
||||
import com.emotion.dto.response.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* AI聊天服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
public interface AiChatService {
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*
|
||||
* @param request AI聊天请求
|
||||
* @return AI聊天响应
|
||||
*/
|
||||
AiChatResponse sendChatMessage(AiChatRequest request);
|
||||
|
||||
/**
|
||||
* 生成对话总结
|
||||
*
|
||||
* @param request AI总结请求
|
||||
* @return AI总结响应
|
||||
*/
|
||||
AiSummaryResponse generateConversationSummary(AiSummaryRequest request);
|
||||
|
||||
/**
|
||||
* 获取AI服务状态
|
||||
*
|
||||
* @return AI状态响应
|
||||
*/
|
||||
AiStatusResponse getServiceStatus();
|
||||
|
||||
/**
|
||||
* 获取聊天统计
|
||||
*
|
||||
* @param request 聊天统计请求
|
||||
* @return 聊天统计响应
|
||||
*/
|
||||
ChatStatsResponse getChatStats(ChatStatsRequest request);
|
||||
|
||||
/**
|
||||
* 访客聊天
|
||||
*
|
||||
* @param request 访客聊天请求
|
||||
* @param clientIp 客户端IP
|
||||
* @return 访客聊天响应
|
||||
*/
|
||||
GuestChatResponse guestChat(GuestChatRequest request, String clientIp);
|
||||
|
||||
/**
|
||||
* 获取访客用户信息
|
||||
*
|
||||
* @param clientIp 客户端IP
|
||||
* @return 访客用户信息响应
|
||||
*/
|
||||
GuestUserInfoResponse getGuestUserInfo(String clientIp);
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
*
|
||||
* @param request 对话创建请求
|
||||
* @param clientIp 客户端IP
|
||||
* @return 对话响应
|
||||
*/
|
||||
ConversationResponse createConversation(ConversationCreateRequest request, String clientIp);
|
||||
|
||||
/**
|
||||
* WebSocket方式发送聊天消息(只保存AI回复)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param message 用户消息内容
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String sendChatMessageForWebSocket(String conversationId, String message, String userId);
|
||||
|
||||
/**
|
||||
* WebSocket方式发送聊天消息(只保存AI回复,带messageId)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param messageId 用户消息ID
|
||||
* @param message 用户消息内容
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String sendChatMessageForWebSocket(String conversationId, String messageId, String message, String userId);
|
||||
|
||||
/**
|
||||
* 发送消息到Coze AI(不保存消息,仅AI交互)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param userMessage 用户消息内容
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String sendMessage(String conversationId, String userMessage, String userId);
|
||||
|
||||
/**
|
||||
* 流式聊天(暂时降级为普通聊天)
|
||||
* @param conversationId 会话ID
|
||||
* @param message 用户消息内容
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String streamChat(String conversationId, String message, String userId);
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* @return 健康返回true,否则false
|
||||
*/
|
||||
boolean healthCheck();
|
||||
|
||||
/**
|
||||
* 发送日记内容生成AI总结评论
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param userMessage 用户日记内容
|
||||
* @param userId 用户ID
|
||||
* @return AI评论内容
|
||||
*/
|
||||
String sendSummaryMessage(String conversationId, String userMessage, String userId);
|
||||
|
||||
/**
|
||||
* 生成用户情绪记录总结
|
||||
* @param userId 用户ID
|
||||
* @return 包含情绪总结等信息的Map
|
||||
*/
|
||||
Map<String, Object> generateEmotionSummary(String userId);
|
||||
|
||||
/**
|
||||
* 异步生成用户情绪记录总结
|
||||
* @param userId 用户ID
|
||||
* @return 包含情绪总结等信息的CompletableFuture
|
||||
*/
|
||||
CompletableFuture<Map<String, Object>> generateEmotionSummaryAsync(String userId);
|
||||
|
||||
/**
|
||||
* 生成用户情绪记录总结并返回响应对象
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 情绪总结生成响应
|
||||
*/
|
||||
EmotionSummaryGenerateResponse generateEmotionSummaryWithResponse(String userId);
|
||||
|
||||
/**
|
||||
* 获取用户情绪记录总结状态并返回响应对象
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 情绪总结状态响应
|
||||
*/
|
||||
EmotionSummaryStatusResponse getEmotionSummaryStatusWithResponse(String userId);
|
||||
|
||||
/**
|
||||
* 通过配置键调用Coze工作流API
|
||||
* 根据config_key从数据库获取AI配置,构建请求并调用Coze工作流接口
|
||||
* 默认使用流式调用方式
|
||||
*
|
||||
* @param configKey AI配置键(如:coze.course.life.generate)
|
||||
* @param input 输入参数,将作为parameters.input传递给工作流
|
||||
* @param userId 用户ID
|
||||
* @return AI生成的内容
|
||||
* @throws RuntimeException 如果配置不存在或已禁用
|
||||
*/
|
||||
String callWorkflowByConfigKey(String configKey, String input, String userId);
|
||||
|
||||
/**
|
||||
* 通过配置键调用Coze工作流API(带自定义参数)
|
||||
* 根据config_key从数据库获取AI配置,将自定义参数与配置中的custom_params合并后调用工作流
|
||||
* 默认使用流式调用方式
|
||||
*
|
||||
* @param configKey AI配置键
|
||||
* @param parameters 自定义参数Map,将合并到请求的parameters中
|
||||
* @param userId 用户ID
|
||||
* @return AI生成的内容
|
||||
* @throws RuntimeException 如果配置不存在或已禁用
|
||||
*/
|
||||
String callWorkflowByConfigKey(String configKey, Map<String, Object> parameters, String userId);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.aiconfig.*;
|
||||
import com.emotion.dto.response.aiconfig.AiConfigResponse;
|
||||
import com.emotion.entity.AiConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI配置服务接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-10-30
|
||||
*/
|
||||
public interface AiConfigService extends IService<AiConfig> {
|
||||
|
||||
/**
|
||||
* 分页查询AI配置
|
||||
*/
|
||||
IPage<AiConfig> getPage(AiConfigPageRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询AI配置并返回响应对象
|
||||
*/
|
||||
PageResult<AiConfigResponse> getPageWithResponse(AiConfigPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取AI配置响应对象
|
||||
*/
|
||||
AiConfigResponse getAiConfigResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建AI配置并返回响应对象
|
||||
*/
|
||||
AiConfigResponse createAiConfigWithResponse(AiConfigCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新AI配置并返回响应对象
|
||||
*/
|
||||
AiConfigResponse updateAiConfigWithResponse(AiConfigUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 根据配置类型查询AI配置
|
||||
*/
|
||||
List<AiConfig> getByConfigType(String configType);
|
||||
|
||||
/**
|
||||
* 根据配置类型查询AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getByConfigTypeWithResponse(String configType);
|
||||
|
||||
/**
|
||||
* 根据服务提供商查询AI配置
|
||||
*/
|
||||
List<AiConfig> getByProvider(String provider);
|
||||
|
||||
/**
|
||||
* 根据服务提供商查询AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getByProviderWithResponse(String provider);
|
||||
|
||||
/**
|
||||
* 根据使用场景查询AI配置
|
||||
*/
|
||||
List<AiConfig> getByUsageScenario(String usageScenario);
|
||||
|
||||
/**
|
||||
* 根据使用场景查询AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getByUsageScenarioWithResponse(String usageScenario);
|
||||
|
||||
/**
|
||||
* 根据环境查询AI配置
|
||||
*/
|
||||
List<AiConfig> getByEnvironment(String environment);
|
||||
|
||||
/**
|
||||
* 根据环境查询AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getByEnvironmentWithResponse(String environment);
|
||||
|
||||
/**
|
||||
* 查询已启用的AI配置
|
||||
*/
|
||||
List<AiConfig> getEnabledConfigs();
|
||||
|
||||
/**
|
||||
* 查询已启用的AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getEnabledConfigsWithResponse();
|
||||
|
||||
/**
|
||||
* 查询已禁用的AI配置
|
||||
*/
|
||||
List<AiConfig> getDisabledConfigs();
|
||||
|
||||
/**
|
||||
* 查询已禁用的AI配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getDisabledConfigsWithResponse();
|
||||
|
||||
/**
|
||||
* 查询默认配置
|
||||
*/
|
||||
List<AiConfig> getDefaultConfigs();
|
||||
|
||||
/**
|
||||
* 查询默认配置并返回响应对象
|
||||
*/
|
||||
List<AiConfigResponse> getDefaultConfigsWithResponse();
|
||||
|
||||
/**
|
||||
* 根据配置键值查询AI配置
|
||||
*/
|
||||
AiConfig getByConfigKey(String configKey);
|
||||
|
||||
/**
|
||||
* 根据配置键值查询AI配置并返回响应对象
|
||||
*/
|
||||
AiConfigResponse getByConfigKeyWithResponse(String configKey);
|
||||
|
||||
/**
|
||||
* 启用AI配置
|
||||
*/
|
||||
boolean enableConfig(String id);
|
||||
|
||||
/**
|
||||
* 禁用AI配置
|
||||
*/
|
||||
boolean disableConfig(String id);
|
||||
|
||||
/**
|
||||
* 设置为默认配置
|
||||
*/
|
||||
boolean setAsDefault(String id);
|
||||
|
||||
/**
|
||||
* 取消默认配置
|
||||
*/
|
||||
boolean unsetDefault(String id);
|
||||
|
||||
/**
|
||||
* 根据使用场景和环境查询最优配置
|
||||
*/
|
||||
AiConfig getBestConfig(String usageScenario, String environment);
|
||||
|
||||
/**
|
||||
* 根据使用场景和环境查询最优配置并返回响应对象
|
||||
*/
|
||||
AiConfigResponse getBestConfigWithResponse(String usageScenario, String environment);
|
||||
|
||||
/**
|
||||
* 统计已启用配置数量
|
||||
*/
|
||||
Long countEnabledConfigs();
|
||||
|
||||
/**
|
||||
* 统计已禁用配置数量
|
||||
*/
|
||||
Long countDisabledConfigs();
|
||||
|
||||
/**
|
||||
* 统计默认配置数量
|
||||
*/
|
||||
Long countDefaultConfigs();
|
||||
|
||||
/**
|
||||
* 根据配置类型统计数量
|
||||
*/
|
||||
Long countByConfigType(String configType);
|
||||
|
||||
/**
|
||||
* 根据服务提供商统计数量
|
||||
*/
|
||||
Long countByProvider(String provider);
|
||||
|
||||
/**
|
||||
* 测试后更新AI配置
|
||||
* 从测试请求中解析参数并更新配置
|
||||
*/
|
||||
AiConfigResponse updateFromTestRequest(AiConfigTestUpdateRequest request);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AiEndpointConfigService extends IService<AiEndpointConfig> {
|
||||
|
||||
List<AiEndpointConfig> listVisible();
|
||||
|
||||
AiEndpointConfig saveEndpoint(AiEndpointConfig endpoint);
|
||||
|
||||
AiEndpointConfig updateEndpoint(AiEndpointConfig endpoint);
|
||||
|
||||
AiEndpointConfig getEnabledById(String id);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.AiProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AiProviderService extends IService<AiProvider> {
|
||||
|
||||
List<AiProvider> listVisible();
|
||||
|
||||
AiProvider saveProvider(AiProvider provider);
|
||||
|
||||
AiProvider updateProvider(AiProvider provider);
|
||||
|
||||
AiProvider getEnabledById(String id);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiTestTemplateResponse;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface AiRuntimeService {
|
||||
|
||||
void invokeStream(AiRuntimeRequest request, Consumer<AiStreamEvent> consumer);
|
||||
|
||||
AiRuntimeTestResponse test(AiRuntimeRequest request);
|
||||
|
||||
AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs);
|
||||
|
||||
void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer);
|
||||
|
||||
AiTestTemplateResponse buildEndpointTestTemplate(String endpointId);
|
||||
|
||||
AiTestTemplateResponse buildSceneTestTemplate(String sceneCode);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.AiSceneBinding;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AiSceneBindingService extends IService<AiSceneBinding> {
|
||||
|
||||
List<AiSceneBinding> listVisible();
|
||||
|
||||
AiSceneBinding resolveScene(String sceneCode);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.dto.request.analytics.AnalyticsEventBatchRequest;
|
||||
import com.emotion.dto.request.analytics.AnalyticsQueryRequest;
|
||||
import com.emotion.dto.response.analytics.AnalyticsBatchResponse;
|
||||
import com.emotion.dto.response.analytics.AnalyticsFunnelItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsOverviewResponse;
|
||||
import com.emotion.dto.response.analytics.AnalyticsPreferenceItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTopEventItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTrendItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsUserItem;
|
||||
import com.emotion.entity.AnalyticsEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AnalyticsService extends IService<AnalyticsEvent> {
|
||||
AnalyticsBatchResponse ingestBatch(AnalyticsEventBatchRequest request);
|
||||
|
||||
AnalyticsOverviewResponse getOverview(AnalyticsQueryRequest request);
|
||||
|
||||
List<AnalyticsTrendItem> getTrend(AnalyticsQueryRequest request);
|
||||
|
||||
List<AnalyticsFunnelItem> getFunnel(AnalyticsQueryRequest request);
|
||||
|
||||
List<AnalyticsPreferenceItem> getPreferences(AnalyticsQueryRequest request);
|
||||
|
||||
List<AnalyticsTopEventItem> getTopEvents(AnalyticsQueryRequest request);
|
||||
|
||||
List<AnalyticsUserItem> getUsers(AnalyticsQueryRequest request);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.emotion.dto.request.ApiEndpointListRequest;
|
||||
import com.emotion.dto.response.ApiEndpointDetailResponse;
|
||||
import com.emotion.dto.response.ApiEndpointItemResponse;
|
||||
|
||||
/**
|
||||
* 接口端点服务
|
||||
*
|
||||
* @author Peanut
|
||||
* @date 2026-05-23
|
||||
*/
|
||||
public interface ApiEndpointService {
|
||||
|
||||
/**
|
||||
* 分页查询接口列表
|
||||
*/
|
||||
IPage<ApiEndpointItemResponse> getPage(ApiEndpointListRequest request);
|
||||
|
||||
/**
|
||||
* 查询接口详情(含参数)
|
||||
*/
|
||||
ApiEndpointDetailResponse getDetail(String operationId);
|
||||
|
||||
/**
|
||||
* 从 OpenAPI spec 同步接口数据
|
||||
*/
|
||||
void syncFromOpenApi();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 启动时自动同步接口数据
|
||||
*
|
||||
* @author Peanut
|
||||
* @date 2026-05-23
|
||||
*/
|
||||
@Component
|
||||
public class ApiEndpointSyncRunner implements ApplicationRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiEndpointSyncRunner.class);
|
||||
|
||||
@Autowired
|
||||
private ApiEndpointService apiEndpointService;
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
log.info("启动同步:开始异步同步接口数据");
|
||||
try {
|
||||
apiEndpointService.syncFromOpenApi();
|
||||
log.info("启动同步:接口数据同步完成");
|
||||
} catch (Exception e) {
|
||||
log.warn("启动同步:接口数据同步失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.response.asr.AsrTranscribeResponse;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface AsrService {
|
||||
|
||||
AsrTranscribeResponse transcribe(MultipartFile file);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
import com.emotion.dto.response.CaptchaResponse;
|
||||
import com.emotion.dto.response.SmsCodeResponse;
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @return 认证响应
|
||||
*/
|
||||
AuthResponse login(LoginRequest request);
|
||||
|
||||
AuthResponse wechatLogin(WechatLoginRequest request);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param request 注册请求
|
||||
* @return 认证响应
|
||||
*/
|
||||
/**
|
||||
* 重置密码(手机号 + 验证码)
|
||||
*
|
||||
* @param request 重置密码请求
|
||||
* @return 重置密码响应
|
||||
*/
|
||||
ResetPasswordResponse resetPassword(ResetPasswordRequest request);
|
||||
|
||||
AuthResponse register(RegisterRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息响应
|
||||
*/
|
||||
UserInfoResponse getCurrentUserInfo(String userId);
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @return 验证码响应
|
||||
*/
|
||||
CaptchaResponse generateCaptcha();
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*
|
||||
* @param captchaKey 验证码key
|
||||
* @param captcha 验证码
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean validateCaptcha(String captchaKey, String captcha);
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param token 访问令牌
|
||||
* @return 是否登出成功
|
||||
*/
|
||||
boolean logout(String userId, String token);
|
||||
|
||||
/**
|
||||
* 用户登出(通过请求)
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 是否登出成功
|
||||
*/
|
||||
boolean logoutByToken(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新的认证响应
|
||||
*/
|
||||
AuthResponse refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 是否有效
|
||||
*/
|
||||
boolean validateToken(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
boolean validateToken(String token);
|
||||
|
||||
/**
|
||||
* 从令牌中获取用户ID
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 用户ID
|
||||
*/
|
||||
String getUserIdFromToken(String token);
|
||||
|
||||
/**
|
||||
* 从令牌中获取用户名
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 用户名
|
||||
*/
|
||||
String getUsernameFromToken(String token);
|
||||
|
||||
/**
|
||||
* 检查账号是否存在
|
||||
*
|
||||
* @param account 账号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByAccount(String account);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 检查手机号是否存在
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 短信验证码响应
|
||||
*/
|
||||
SmsCodeResponse sendSmsCode(String phone);
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param code 验证码
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean validateSmsCode(String phone, String code);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.comment.CommentCreateRequest;
|
||||
import com.emotion.dto.request.comment.CommentUpdateRequest;
|
||||
import com.emotion.dto.request.comment.CommentPageRequest;
|
||||
import com.emotion.dto.response.comment.CommentResponse;
|
||||
import com.emotion.entity.Comment;
|
||||
|
||||
/**
|
||||
* 评论服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface CommentService extends IService<Comment> {
|
||||
|
||||
/**
|
||||
* 分页查询评论
|
||||
*/
|
||||
PageResult<CommentResponse> getPage(CommentPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取评论响应
|
||||
*/
|
||||
CommentResponse getById(String id);
|
||||
|
||||
/**
|
||||
* 创建评论
|
||||
*/
|
||||
CommentResponse create(CommentCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新评论
|
||||
*/
|
||||
CommentResponse update(CommentUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
boolean delete(String id);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.community.CommunityPostCreateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostUpdateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostPageRequest;
|
||||
import com.emotion.dto.response.community.CommunityPostResponse;
|
||||
import com.emotion.entity.CommunityPost;
|
||||
|
||||
/**
|
||||
* 社区帖子服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface CommunityPostService extends IService<CommunityPost> {
|
||||
|
||||
/**
|
||||
* 分页查询帖子
|
||||
*/
|
||||
PageResult<CommunityPostResponse> getPage(CommunityPostPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取帖子响应
|
||||
*/
|
||||
CommunityPostResponse getById(String id);
|
||||
|
||||
/**
|
||||
* 创建帖子
|
||||
*/
|
||||
CommunityPostResponse create(CommunityPostCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新帖子
|
||||
*/
|
||||
CommunityPostResponse update(CommunityPostUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除帖子
|
||||
*/
|
||||
boolean delete(String id);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.ConversationCreateRequest;
|
||||
import com.emotion.dto.request.ConversationPageRequest;
|
||||
import com.emotion.dto.response.ConversationResponse;
|
||||
import com.emotion.entity.Conversation;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface ConversationService extends IService<Conversation> {
|
||||
|
||||
/**
|
||||
* 分页查询会话
|
||||
*/
|
||||
IPage<Conversation> getPage(ConversationPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询会话
|
||||
*/
|
||||
IPage<Conversation> getPageByUserId(ConversationPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话列表
|
||||
*/
|
||||
List<Conversation> getByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询活跃会话列表
|
||||
*/
|
||||
List<Conversation> getActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据Coze会话ID查询会话
|
||||
*/
|
||||
Conversation getByCozeConversationId(String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 更新会话消息数量
|
||||
*/
|
||||
boolean updateMessageCount(String conversationId, Integer messageCount);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*/
|
||||
boolean updateStatus(String conversationId, Integer status);
|
||||
|
||||
/**
|
||||
* 更新会话结束时间
|
||||
*/
|
||||
boolean updateEndTime(String conversationId, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的会话数量
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户的活跃会话数量
|
||||
*/
|
||||
Long countActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 查询需要归档的会话(超过指定天数未活跃)
|
||||
*/
|
||||
List<Conversation> getForArchive(Integer days);
|
||||
|
||||
/**
|
||||
* 批量归档会话
|
||||
*/
|
||||
boolean batchArchive(List<String> conversationIds);
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
Conversation createConversation(String userId, String title, String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
*/
|
||||
boolean endConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* 分页查询会话响应
|
||||
*/
|
||||
PageResult<ConversationResponse> getPageWithResponse(ConversationPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询会话响应
|
||||
*/
|
||||
PageResult<ConversationResponse> getPageByUserIdWithResponse(ConversationPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取会话响应
|
||||
*/
|
||||
ConversationResponse getConversationResponseById(String id);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话响应列表
|
||||
*/
|
||||
List<ConversationResponse> getByUserIdWithResponse(String userId);
|
||||
|
||||
/**
|
||||
* 获取活跃会话响应列表
|
||||
*/
|
||||
List<ConversationResponse> getActiveConversationsWithResponse();
|
||||
|
||||
/**
|
||||
* 获取归档会话响应列表
|
||||
*/
|
||||
List<ConversationResponse> getArchivedConversationsWithResponse();
|
||||
|
||||
/**
|
||||
* 归档对话
|
||||
*/
|
||||
boolean archiveConversation(String id);
|
||||
|
||||
/**
|
||||
* 激活对话
|
||||
*/
|
||||
boolean activateConversation(String id);
|
||||
|
||||
/**
|
||||
* 创建会话并返回响应对象
|
||||
*/
|
||||
ConversationResponse createConversationWithResponse(ConversationCreateRequest request,
|
||||
HttpServletRequest httpRequest);
|
||||
|
||||
/**
|
||||
* 更新会话并返回响应对象
|
||||
*/
|
||||
ConversationResponse updateConversationWithResponse(ConversationCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*/
|
||||
boolean updateConversationStatus(String id, String status);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.coze.CozeApiCallPageRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallCreateRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallUpdateRequest;
|
||||
import com.emotion.dto.response.coze.CozeApiCallResponse;
|
||||
import com.emotion.entity.CozeApiCall;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Coze API调用记录服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface CozeApiCallService extends IService<CozeApiCall> {
|
||||
|
||||
/**
|
||||
* 分页查询API调用记录
|
||||
*/
|
||||
PageResult<CozeApiCallResponse> getPage(CozeApiCallPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取API调用记录
|
||||
*/
|
||||
CozeApiCallResponse getById(String id);
|
||||
|
||||
/**
|
||||
* 创建API调用记录
|
||||
*/
|
||||
CozeApiCallResponse create(CozeApiCallCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新API调用记录
|
||||
*/
|
||||
CozeApiCallResponse update(CozeApiCallUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除API调用记录
|
||||
*/
|
||||
boolean delete(String id);
|
||||
|
||||
/**
|
||||
* 统计用户的API调用次数
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计Bot的API调用次数
|
||||
*/
|
||||
Long countByBotId(String botId);
|
||||
|
||||
/**
|
||||
* 统计指定状态的API调用次数
|
||||
*/
|
||||
Long countByStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计用户的Token使用量
|
||||
*/
|
||||
Long sumTokensByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户的API调用费用
|
||||
*/
|
||||
BigDecimal sumCostByUserId(String userId);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.AiConfigCallStatsRequest;
|
||||
import com.emotion.dto.response.AiConfigCallStatsResponse;
|
||||
import com.emotion.dto.response.DashboardStatsResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 仪表盘服务接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-10-31
|
||||
*/
|
||||
public interface DashboardService {
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*
|
||||
* @return 仪表盘统计数据
|
||||
*/
|
||||
DashboardStatsResponse getDashboardStats();
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
*
|
||||
* @return 用户统计数据
|
||||
*/
|
||||
DashboardStatsResponse.UserStats getUserStats();
|
||||
|
||||
/**
|
||||
* 获取内容统计数据
|
||||
*
|
||||
* @return 内容统计数据
|
||||
*/
|
||||
DashboardStatsResponse.ContentStats getContentStats();
|
||||
|
||||
/**
|
||||
* 获取AI服务统计数据
|
||||
*
|
||||
* @return AI服务统计数据
|
||||
*/
|
||||
DashboardStatsResponse.AiServiceStats getAiServiceStats();
|
||||
|
||||
/**
|
||||
* 获取系统统计数据
|
||||
*
|
||||
* @return 系统统计数据
|
||||
*/
|
||||
DashboardStatsResponse.SystemStats getSystemStats();
|
||||
|
||||
/**
|
||||
* 获取用户增长趋势数据
|
||||
*
|
||||
* @param days 天数,默认7天
|
||||
* @return 用户增长趋势数据
|
||||
*/
|
||||
List<DashboardStatsResponse.UserGrowthTrend> getUserGrowthTrends(int days);
|
||||
|
||||
/**
|
||||
* 获取最近登录用户
|
||||
*
|
||||
* @param limit 限制数量,默认10个
|
||||
* @return 最近登录用户列表
|
||||
*/
|
||||
List<DashboardStatsResponse.RecentLogin> getRecentLogins(int limit);
|
||||
|
||||
/**
|
||||
* 获取 AI 配置调用次数统计(按调用次数倒序)
|
||||
*
|
||||
* @param request 统计请求
|
||||
* @return AI配置调用次数统计响应
|
||||
*/
|
||||
AiConfigCallStatsResponse getAiConfigCallStats(AiConfigCallStatsRequest request);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.DiaryCommentCreateRequest;
|
||||
import com.emotion.dto.request.DiaryCommentPageRequest;
|
||||
import com.emotion.dto.response.DiaryCommentResponse;
|
||||
import com.emotion.entity.DiaryComment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日记评论服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface DiaryCommentService extends IService<DiaryComment> {
|
||||
|
||||
/**
|
||||
* 分页查询评论
|
||||
*/
|
||||
PageResult<DiaryCommentResponse> getPageWithResponse(DiaryCommentPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取评论响应
|
||||
*/
|
||||
DiaryCommentResponse getCommentResponseById(String id);
|
||||
|
||||
/**
|
||||
* 获取评论树结构响应
|
||||
*/
|
||||
List<DiaryCommentResponse> getCommentTreeWithResponse(String diaryId);
|
||||
|
||||
/**
|
||||
* 创建评论并返回响应对象
|
||||
*/
|
||||
DiaryCommentResponse createCommentWithResponse(DiaryCommentCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新评论并返回响应对象
|
||||
*/
|
||||
DiaryCommentResponse updateCommentWithResponse(DiaryCommentCreateRequest request);
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
boolean deleteComment(String commentId);
|
||||
|
||||
/**
|
||||
* 软删除评论
|
||||
*/
|
||||
boolean softDeleteComment(String commentId);
|
||||
|
||||
/**
|
||||
* 恢复评论
|
||||
*/
|
||||
boolean restoreComment(String commentId);
|
||||
|
||||
/**
|
||||
* 增加点赞数
|
||||
*/
|
||||
boolean incrementLikeCount(String commentId);
|
||||
|
||||
/**
|
||||
* 减少点赞数
|
||||
*/
|
||||
boolean decrementLikeCount(String commentId);
|
||||
|
||||
/**
|
||||
* 设置置顶状态
|
||||
*/
|
||||
boolean setTop(String commentId, Integer isTop);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.DiaryPostCreateRequest;
|
||||
import com.emotion.dto.request.DiaryPostPageRequest;
|
||||
import com.emotion.dto.request.DiaryPostUpdateRequest;
|
||||
import com.emotion.dto.response.DiaryPostResponse;
|
||||
import com.emotion.entity.DiaryPost;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户日记服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface DiaryPostService extends IService<DiaryPost> {
|
||||
|
||||
/**
|
||||
* 分页查询日记
|
||||
*/
|
||||
PageResult<DiaryPostResponse> getPageWithResponse(DiaryPostPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取日记响应
|
||||
*/
|
||||
DiaryPostResponse getDiaryPostResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建日记并返回响应对象
|
||||
*/
|
||||
DiaryPostResponse createDiaryPostWithResponse(DiaryPostCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新日记并返回响应对象
|
||||
*/
|
||||
DiaryPostResponse updateDiaryPostWithResponse(DiaryPostUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除日记
|
||||
*/
|
||||
boolean deleteDiaryPost(String diaryId);
|
||||
|
||||
/**
|
||||
* 软删除日记
|
||||
*/
|
||||
boolean softDeleteDiaryPost(String diaryId);
|
||||
|
||||
/**
|
||||
* 恢复日记
|
||||
*/
|
||||
boolean restoreDiaryPost(String diaryId);
|
||||
|
||||
/**
|
||||
* 增加浏览数
|
||||
*/
|
||||
boolean incrementViewCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 增加点赞数
|
||||
*/
|
||||
boolean incrementLikeCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 减少点赞数
|
||||
*/
|
||||
boolean decrementLikeCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 增加评论数
|
||||
*/
|
||||
boolean incrementCommentCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 减少评论数
|
||||
*/
|
||||
boolean decrementCommentCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 增加分享数
|
||||
*/
|
||||
boolean incrementShareCount(String diaryId);
|
||||
|
||||
/**
|
||||
* 更新最后评论时间
|
||||
*/
|
||||
boolean updateLastCommentTime(String diaryId);
|
||||
|
||||
/**
|
||||
* 设置精选状态
|
||||
*/
|
||||
boolean setFeatured(String diaryId, Integer featured);
|
||||
|
||||
/**
|
||||
* 设置置顶优先级
|
||||
*/
|
||||
boolean setPriority(String diaryId, Integer priority);
|
||||
|
||||
/**
|
||||
* 统计用户日记数量
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户公开日记数量
|
||||
*/
|
||||
Long countPublicByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计精选日记数量
|
||||
*/
|
||||
Long countFeatured();
|
||||
|
||||
/**
|
||||
* 发表日记并生成AI评论
|
||||
*/
|
||||
DiaryPostResponse publishDiaryWithAiComment(DiaryPostCreateRequest request);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.Dictionary;
|
||||
import com.emotion.dto.request.dictionary.DictionaryCreateRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryPageRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryUpdateRequest;
|
||||
import com.emotion.dto.response.dictionary.DictionaryResponse;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 字典Service接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
public interface DictionaryService extends IService<Dictionary> {
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 创建结果
|
||||
*/
|
||||
Result<DictionaryResponse> createDictionary(DictionaryCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新字典
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 更新结果
|
||||
*/
|
||||
Result<DictionaryResponse> updateDictionary(DictionaryUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
Result<Void> deleteDictionary(String id);
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 字典详情
|
||||
*/
|
||||
Result<DictionaryResponse> getDictionary(String id);
|
||||
|
||||
/**
|
||||
* 分页查询字典
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据字典类型查询字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 字典集合
|
||||
*/
|
||||
Result<List<DictionaryResponse>> getDictionariesByType(String dictType);
|
||||
|
||||
/**
|
||||
* 根据字典类型查询启用的字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 启用的字典集合
|
||||
*/
|
||||
Result<List<DictionaryResponse>> getEnabledDictionariesByType(String dictType);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.EmotionAnalysisCreateRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisPageRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionAnalysisResponse;
|
||||
import com.emotion.entity.EmotionAnalysis;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 情绪分析服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface EmotionAnalysisService extends IService<EmotionAnalysis> {
|
||||
|
||||
/**
|
||||
* 分页查询情绪分析记录
|
||||
*/
|
||||
IPage<EmotionAnalysis> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询情绪分析记录
|
||||
*/
|
||||
IPage<EmotionAnalysis> getPageByUserId(BasePageRequest request, String userId);
|
||||
|
||||
/**
|
||||
* 根据消息ID查询情绪分析记录
|
||||
*/
|
||||
EmotionAnalysis getByMessageId(String messageId);
|
||||
|
||||
/**
|
||||
* 创建情绪分析记录
|
||||
*/
|
||||
EmotionAnalysis createEmotionAnalysis(String messageId, String userId, String primaryEmotion,
|
||||
String polarity, Double intensity, Double confidence);
|
||||
|
||||
// 新增的方法
|
||||
|
||||
/**
|
||||
* 分页查询情绪分析记录响应
|
||||
*/
|
||||
PageResult<EmotionAnalysisResponse> getPageWithResponse(EmotionAnalysisPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询情绪分析记录响应
|
||||
*/
|
||||
PageResult<EmotionAnalysisResponse> getPageByUserIdWithResponse(String userId, EmotionAnalysisPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取情绪分析记录响应
|
||||
*/
|
||||
EmotionAnalysisResponse getEmotionAnalysisResponseById(String id);
|
||||
|
||||
/**
|
||||
* 根据消息ID获取情绪分析记录响应
|
||||
*/
|
||||
EmotionAnalysisResponse getEmotionAnalysisResponseByMessageId(String messageId);
|
||||
|
||||
/**
|
||||
* 创建情绪分析记录并返回响应
|
||||
*/
|
||||
EmotionAnalysisResponse createEmotionAnalysisWithResponse(EmotionAnalysisCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新情绪分析记录并返回响应
|
||||
*/
|
||||
EmotionAnalysisResponse updateEmotionAnalysisWithResponse(EmotionAnalysisUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除情绪分析记录
|
||||
*/
|
||||
boolean deleteEmotionAnalysis(String id);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.EmotionRecordCreateRequest;
|
||||
import com.emotion.dto.request.EmotionRecordPageRequest;
|
||||
import com.emotion.dto.request.EmotionRecordUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionRecordResponse;
|
||||
import com.emotion.entity.EmotionRecord;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 情绪记录服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface EmotionRecordService extends IService<EmotionRecord> {
|
||||
|
||||
/**
|
||||
* 分页查询情绪记录
|
||||
*/
|
||||
IPage<EmotionRecord> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询情绪记录
|
||||
*/
|
||||
IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId);
|
||||
|
||||
/**
|
||||
* 创建情绪记录
|
||||
*/
|
||||
EmotionRecord createEmotionRecord(String userId, String emotionType, Double intensity,
|
||||
String trigger, String location, String notes);
|
||||
|
||||
// 新增的方法
|
||||
|
||||
/**
|
||||
* 分页查询情绪记录响应
|
||||
*/
|
||||
PageResult<EmotionRecordResponse> getPageWithResponse(EmotionRecordPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询情绪记录响应
|
||||
*/
|
||||
PageResult<EmotionRecordResponse> getPageByUserIdWithResponse(String userId, EmotionRecordPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取情绪记录响应
|
||||
*/
|
||||
EmotionRecordResponse getEmotionRecordResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建情绪记录并返回响应
|
||||
*/
|
||||
EmotionRecordResponse createEmotionRecordWithResponse(EmotionRecordCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新情绪记录并返回响应
|
||||
*/
|
||||
EmotionRecordResponse updateEmotionRecordWithResponse(EmotionRecordUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除情绪记录
|
||||
*/
|
||||
boolean deleteEmotionRecord(String id);
|
||||
|
||||
/**
|
||||
* 获取用户情绪统计
|
||||
*/
|
||||
Map<String, Object> getEmotionStats(String userId, String startDate, String endDate);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.EpicScriptCreateRequest;
|
||||
import com.emotion.dto.request.EpicScriptInspirationRequest;
|
||||
import com.emotion.dto.request.EpicScriptPageRequest;
|
||||
import com.emotion.dto.request.EpicScriptUpdateRequest;
|
||||
import com.emotion.dto.response.EpicScriptInspirationResponse;
|
||||
import com.emotion.dto.response.EpicScriptResponse;
|
||||
import com.emotion.dto.response.InspirationSuggestionResponse;
|
||||
import com.emotion.entity.EpicScript;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 爽文剧本服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
public interface EpicScriptService extends IService<EpicScript> {
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的剧本
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<EpicScriptResponse> getPageByCurrentUser(EpicScriptPageRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有剧本列表
|
||||
*
|
||||
* @return 剧本列表
|
||||
*/
|
||||
List<EpicScriptResponse> getListByCurrentUser();
|
||||
|
||||
/**
|
||||
* 根据ID获取剧本
|
||||
*
|
||||
* @param id 剧本ID
|
||||
* @return 剧本响应
|
||||
*/
|
||||
EpicScriptResponse getScriptById(String id);
|
||||
|
||||
/**
|
||||
* 创建剧本
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 剧本响应
|
||||
*/
|
||||
EpicScriptResponse createScript(EpicScriptCreateRequest request);
|
||||
|
||||
List<InspirationSuggestionResponse> getInspirationRecommendations();
|
||||
|
||||
List<InspirationSuggestionResponse> getRandomInspirations(Integer size);
|
||||
|
||||
EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request);
|
||||
|
||||
/**
|
||||
* 更新剧本
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 剧本响应
|
||||
*/
|
||||
EpicScriptResponse updateScript(EpicScriptUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 选中剧本
|
||||
*
|
||||
* @param id 剧本ID
|
||||
* @return 剧本响应
|
||||
*/
|
||||
EpicScriptResponse selectScript(String id);
|
||||
|
||||
/**
|
||||
* 删除剧本
|
||||
*
|
||||
* @param id 剧本ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteScript(String id);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.growth.GrowthTopicCreateRequest;
|
||||
import com.emotion.dto.request.growth.GrowthTopicUpdateRequest;
|
||||
import com.emotion.dto.response.growth.GrowthTopicResponse;
|
||||
import com.emotion.entity.GrowthTopic;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 成长话题服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface GrowthTopicService extends IService<GrowthTopic> {
|
||||
|
||||
/**
|
||||
* 分页查询成长话题
|
||||
*/
|
||||
IPage<GrowthTopic> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询成长话题响应
|
||||
*/
|
||||
PageResult<GrowthTopicResponse> getPageWithResponse(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取成长话题响应
|
||||
*/
|
||||
GrowthTopicResponse getGrowthTopicResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建成长话题
|
||||
*/
|
||||
GrowthTopic createGrowthTopic(String title, String description, String category,
|
||||
String difficultyLevel, String tags, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 创建成长话题并返回响应
|
||||
*/
|
||||
GrowthTopicResponse createGrowthTopicWithResponse(GrowthTopicCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新成长话题并返回响应
|
||||
*/
|
||||
GrowthTopicResponse updateGrowthTopicWithResponse(GrowthTopicUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除成长话题
|
||||
*/
|
||||
boolean deleteGrowthTopic(String id);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.guest.GuestUserCreateRequest;
|
||||
import com.emotion.dto.request.guest.GuestUserUpdateRequest;
|
||||
import com.emotion.dto.response.guest.GuestUserResponse;
|
||||
import com.emotion.entity.GuestUser;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 访客用户服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface GuestUserService extends IService<GuestUser> {
|
||||
|
||||
/**
|
||||
* 分页查询访客用户
|
||||
*/
|
||||
IPage<GuestUser> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询访客用户响应
|
||||
*/
|
||||
PageResult<GuestUserResponse> getPageWithResponse(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取访客用户响应
|
||||
*/
|
||||
GuestUserResponse getGuestUserResponseById(String id);
|
||||
|
||||
/**
|
||||
* 根据设备ID查询访客用户
|
||||
*/
|
||||
GuestUser getByDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* 根据IP地址查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByIpAddress(String ipAddress);
|
||||
|
||||
/**
|
||||
* 根据用户代理查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByUserAgent(String userAgent);
|
||||
|
||||
/**
|
||||
* 根据状态查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByStatus(String status);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据最后活跃时间范围查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByLastActiveTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计指定状态的访客用户数量
|
||||
*/
|
||||
Long countByStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计指定IP地址的访客用户数量
|
||||
*/
|
||||
Long countByIpAddress(String ipAddress);
|
||||
|
||||
/**
|
||||
* 统计今日新增访客用户
|
||||
*/
|
||||
Long countTodayNewGuests();
|
||||
|
||||
/**
|
||||
* 统计活跃访客用户
|
||||
*/
|
||||
Long countActiveGuests(Integer days);
|
||||
|
||||
/**
|
||||
* 查询最近访问的访客用户
|
||||
*/
|
||||
List<GuestUser> getRecentVisitors(Integer limit);
|
||||
|
||||
/**
|
||||
* 查询长时间未活跃的访客用户
|
||||
*/
|
||||
List<GuestUser> getInactiveGuests(Integer days);
|
||||
|
||||
/**
|
||||
* 根据访问次数范围查询访客用户
|
||||
*/
|
||||
List<GuestUser> getByVisitCountRange(Integer minVisits, Integer maxVisits);
|
||||
|
||||
/**
|
||||
* 查询平均访问次数
|
||||
*/
|
||||
Double getAvgVisitCount();
|
||||
|
||||
/**
|
||||
* 更新访客用户最后活跃时间
|
||||
*/
|
||||
boolean updateLastActiveTime(String id, LocalDateTime lastActiveTime);
|
||||
|
||||
/**
|
||||
* 更新访客用户访问次数
|
||||
*/
|
||||
boolean incrementVisitCount(String id);
|
||||
|
||||
/**
|
||||
* 更新访客用户状态
|
||||
*/
|
||||
boolean updateStatus(String id, String status);
|
||||
|
||||
/**
|
||||
* 根据设备信息查询或创建访客用户
|
||||
*/
|
||||
GuestUser getOrCreateByDeviceInfo(String deviceId, String ipAddress, String userAgent);
|
||||
|
||||
/**
|
||||
* 清理过期的访客用户数据
|
||||
*/
|
||||
boolean cleanExpiredGuests(Integer days);
|
||||
|
||||
/**
|
||||
* 创建访客用户
|
||||
*/
|
||||
GuestUser createGuestUser(String deviceId, String ipAddress, String userAgent, String location);
|
||||
|
||||
/**
|
||||
* 创建访客用户并返回响应
|
||||
*/
|
||||
GuestUserResponse createGuestUserWithResponse(GuestUserCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新访客用户并返回响应
|
||||
*/
|
||||
GuestUserResponse updateGuestUserWithResponse(GuestUserUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除访客用户
|
||||
*/
|
||||
boolean deleteGuestUser(String id);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.LifeEventCreateRequest;
|
||||
import com.emotion.dto.request.LifeEventPageRequest;
|
||||
import com.emotion.dto.request.LifeEventUpdateRequest;
|
||||
import com.emotion.dto.response.LifeEventResponse;
|
||||
import com.emotion.entity.LifeEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 生命事件服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
public interface LifeEventService extends IService<LifeEvent> {
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的生命事件
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<LifeEventResponse> getPageByCurrentUser(LifeEventPageRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有生命事件列表
|
||||
*
|
||||
* @return 事件列表
|
||||
*/
|
||||
List<LifeEventResponse> getListByCurrentUser();
|
||||
|
||||
/**
|
||||
* 根据ID获取生命事件
|
||||
*
|
||||
* @param id 事件ID
|
||||
* @return 事件响应
|
||||
*/
|
||||
LifeEventResponse getEventById(String id);
|
||||
|
||||
/**
|
||||
* 创建生命事件
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 事件响应
|
||||
*/
|
||||
LifeEventResponse createEvent(LifeEventCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新生命事件
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 事件响应
|
||||
*/
|
||||
LifeEventResponse updateEvent(LifeEventUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除生命事件
|
||||
*
|
||||
* @param id 事件ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteEvent(String id);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.LifePathCreateRequest;
|
||||
import com.emotion.dto.request.LifePathPageRequest;
|
||||
import com.emotion.dto.request.LifePathUpdateRequest;
|
||||
import com.emotion.dto.response.LifePathResponse;
|
||||
import com.emotion.entity.LifePath;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 实现路径服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
public interface LifePathService extends IService<LifePath> {
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的路径
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<LifePathResponse> getPageByCurrentUser(LifePathPageRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有路径列表
|
||||
*
|
||||
* @return 路径列表
|
||||
*/
|
||||
List<LifePathResponse> getListByCurrentUser();
|
||||
|
||||
/**
|
||||
* 根据剧本ID获取路径
|
||||
*
|
||||
* @param scriptId 剧本ID
|
||||
* @return 路径响应
|
||||
*/
|
||||
LifePathResponse getByScriptId(String scriptId);
|
||||
|
||||
/**
|
||||
* 根据ID获取路径
|
||||
*
|
||||
* @param id 路径ID
|
||||
* @return 路径响应
|
||||
*/
|
||||
LifePathResponse getPathById(String id);
|
||||
|
||||
/**
|
||||
* 创建路径
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 路径响应
|
||||
*/
|
||||
LifePathResponse createPath(LifePathCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新路径
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 路径响应
|
||||
*/
|
||||
LifePathResponse updatePath(LifePathUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除路径
|
||||
*
|
||||
* @param id 路径ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deletePath(String id);
|
||||
|
||||
/**
|
||||
* 根据剧本ID删除路径
|
||||
*
|
||||
* @param scriptId 剧本ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteByScriptId(String scriptId);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.MessagePageRequest;
|
||||
import com.emotion.dto.request.MessageSearchRequest;
|
||||
import com.emotion.dto.request.MessageRecentRequest;
|
||||
import com.emotion.dto.request.MessageCreateRequest;
|
||||
import com.emotion.dto.response.MessageResponse;
|
||||
import com.emotion.entity.Message;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface MessageService extends IService<Message> {
|
||||
|
||||
/**
|
||||
* 分页查询消息
|
||||
*/
|
||||
IPage<Message> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询消息响应
|
||||
*/
|
||||
PageResult<MessageResponse> getPageWithResponse(MessagePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查询消息
|
||||
*/
|
||||
IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*/
|
||||
List<Message> getByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据发送者查询消息列表
|
||||
*/
|
||||
List<Message> getBySender(String sender);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询消息
|
||||
*/
|
||||
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据用户ID和时间范围查询消息
|
||||
*/
|
||||
List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询消息
|
||||
*/
|
||||
IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size);
|
||||
|
||||
/**
|
||||
* 根据用户ID和关键词搜索消息
|
||||
*/
|
||||
List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据用户ID获取最近的消息
|
||||
*/
|
||||
List<Message> getRecentByUserId(String userId, Integer limit);
|
||||
|
||||
/**
|
||||
* 查询会话的最后一条消息
|
||||
*/
|
||||
Message getLastMessageByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据父消息ID查询回复消息
|
||||
*/
|
||||
List<Message> getRepliesByParentId(String parentMessageId);
|
||||
|
||||
/**
|
||||
* 统计会话的消息数量
|
||||
*/
|
||||
Long countByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计发送者的消息数量
|
||||
*/
|
||||
Long countBySender(String sender);
|
||||
|
||||
/**
|
||||
* 查询未读消息数量
|
||||
*/
|
||||
Long countUnreadMessages(String conversationId);
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
boolean updateStatus(String messageId, String status);
|
||||
|
||||
/**
|
||||
* 更新消息已读状态
|
||||
*/
|
||||
boolean updateReadStatus(String messageId, Integer isRead);
|
||||
|
||||
/**
|
||||
* 批量更新会话消息为已读
|
||||
*/
|
||||
boolean markConversationMessagesAsRead(String conversationId);
|
||||
|
||||
/**
|
||||
* 创建消息
|
||||
*/
|
||||
Message createMessage(Message message);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*/
|
||||
boolean markAsRead(String messageId);
|
||||
|
||||
/**
|
||||
* 获取用户消息分页(新接口)
|
||||
*/
|
||||
PageResult<MessageResponse> getUserMessagesWithPage(MessagePageRequest request);
|
||||
|
||||
/**
|
||||
* 搜索用户消息(新接口)
|
||||
*/
|
||||
List<MessageResponse> searchUserMessages(MessageSearchRequest request);
|
||||
|
||||
/**
|
||||
* 获取用户最近消息(新接口)
|
||||
*/
|
||||
List<MessageResponse> getUserRecentMessages(MessageRecentRequest request);
|
||||
|
||||
/**
|
||||
* 根据请求创建消息(新接口)
|
||||
*/
|
||||
MessageResponse createMessageFromRequest(MessageCreateRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取消息响应(新接口)
|
||||
*/
|
||||
MessageResponse getMessageById(String id);
|
||||
|
||||
/**
|
||||
* 搜索消息并返回响应
|
||||
*/
|
||||
PageResult<MessageResponse> searchWithResponse(MessageSearchRequest request);
|
||||
|
||||
/**
|
||||
* 获取最近消息并返回响应
|
||||
*/
|
||||
PageResult<MessageResponse> getRecentWithResponse(MessageRecentRequest request);
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
*/
|
||||
MessageResponse updateMessage(String id, String content);
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
boolean deleteMessage(String id);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.reward.RewardCreateRequest;
|
||||
import com.emotion.dto.request.reward.RewardUpdateRequest;
|
||||
import com.emotion.dto.response.reward.RewardResponse;
|
||||
import com.emotion.entity.Reward;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 奖励服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface RewardService extends IService<Reward> {
|
||||
|
||||
/**
|
||||
* 分页查询奖励
|
||||
*/
|
||||
IPage<Reward> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询奖励响应
|
||||
*/
|
||||
PageResult<RewardResponse> getPageWithResponse(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询奖励
|
||||
*/
|
||||
IPage<Reward> getPageByUserId(BasePageRequest request, String userId);
|
||||
|
||||
/**
|
||||
* 根据ID获取奖励响应
|
||||
*/
|
||||
RewardResponse getRewardResponseById(String id);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询奖励
|
||||
*/
|
||||
List<Reward> getByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据奖励类型查询奖励
|
||||
*/
|
||||
List<Reward> getByRewardType(String rewardType);
|
||||
|
||||
/**
|
||||
* 根据用户ID和奖励类型查询奖励
|
||||
*/
|
||||
List<Reward> getByUserIdAndRewardType(String userId, String rewardType);
|
||||
|
||||
/**
|
||||
* 根据状态查询奖励
|
||||
*/
|
||||
List<Reward> getByStatus(String status);
|
||||
|
||||
/**
|
||||
* 根据用户ID和状态查询奖励
|
||||
*/
|
||||
List<Reward> getByUserIdAndStatus(String userId, String status);
|
||||
|
||||
/**
|
||||
* 根据积分范围查询奖励
|
||||
*/
|
||||
List<Reward> getByPointsRange(Integer minPoints, Integer maxPoints);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询奖励
|
||||
*/
|
||||
List<Reward> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据获得时间范围查询奖励
|
||||
*/
|
||||
List<Reward> getByEarnedTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的奖励数量
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计指定类型的奖励数量
|
||||
*/
|
||||
Long countByRewardType(String rewardType);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型的奖励数量
|
||||
*/
|
||||
Long countByUserIdAndRewardType(String userId, String rewardType);
|
||||
|
||||
/**
|
||||
* 统计指定状态的奖励数量
|
||||
*/
|
||||
Long countByStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计用户的总积分
|
||||
*/
|
||||
Integer sumPointsByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型的总积分
|
||||
*/
|
||||
Integer sumPointsByUserIdAndRewardType(String userId, String rewardType);
|
||||
|
||||
/**
|
||||
* 查询用户最近获得的奖励
|
||||
*/
|
||||
List<Reward> getRecentByUserId(String userId, Integer limit);
|
||||
|
||||
/**
|
||||
* 查询高积分奖励
|
||||
*/
|
||||
List<Reward> getHighPointsRewards(Integer minPoints);
|
||||
|
||||
/**
|
||||
* 查询待领取的奖励
|
||||
*/
|
||||
List<Reward> getPendingRewardsByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 查询已领取的奖励
|
||||
*/
|
||||
List<Reward> getClaimedRewardsByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 查询已过期的奖励
|
||||
*/
|
||||
List<Reward> getExpiredRewards();
|
||||
|
||||
/**
|
||||
* 更新奖励状态
|
||||
*/
|
||||
boolean updateStatus(String id, String status, LocalDateTime claimedTime);
|
||||
|
||||
/**
|
||||
* 批量更新过期奖励状态
|
||||
*/
|
||||
boolean updateExpiredRewards();
|
||||
|
||||
/**
|
||||
* 根据来源查询奖励
|
||||
*/
|
||||
List<Reward> getBySource(String source);
|
||||
|
||||
/**
|
||||
* 创建奖励
|
||||
*/
|
||||
Reward createReward(String userId, String rewardType, String value,
|
||||
String description, String topicId, String achievementId, String icon, String rarity);
|
||||
|
||||
/**
|
||||
* 创建奖励并返回响应
|
||||
*/
|
||||
RewardResponse createRewardWithResponse(RewardCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新奖励并返回响应
|
||||
*/
|
||||
RewardResponse updateRewardWithResponse(RewardUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除奖励
|
||||
*/
|
||||
boolean deleteReward(String id);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ScriptContextService {
|
||||
|
||||
String buildSocialInsightContext(String userId, Boolean useSocialInsights);
|
||||
|
||||
List<String> getConfirmedInsightLabels(String userId);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.dto.request.social.SocialContentLinkImportRequest;
|
||||
import com.emotion.dto.request.social.SocialContentManualImportRequest;
|
||||
import com.emotion.dto.response.social.SocialContentItemResponse;
|
||||
import com.emotion.entity.SocialContentItem;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SocialContentService extends IService<SocialContentItem> {
|
||||
|
||||
SocialContentItemResponse manualImport(String userId, SocialContentManualImportRequest request);
|
||||
|
||||
SocialContentItemResponse linkImport(String userId, SocialContentLinkImportRequest request);
|
||||
|
||||
SocialContentItemResponse screenshotImport(String userId, String platform, MultipartFile file);
|
||||
|
||||
List<SocialContentItemResponse> listByUser(String userId);
|
||||
|
||||
boolean deleteByUser(String userId, String id, Boolean keepConfirmedInsights);
|
||||
|
||||
SocialContentItemResponse updateApproval(String userId, String id, Boolean approvedForAi);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.dto.request.social.SocialInsightGenerateRequest;
|
||||
import com.emotion.dto.request.social.SocialInsightUpdateRequest;
|
||||
import com.emotion.dto.response.social.SocialProfileInsightResponse;
|
||||
import com.emotion.entity.SocialProfileInsight;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SocialInsightService extends IService<SocialProfileInsight> {
|
||||
|
||||
List<SocialProfileInsightResponse> generateInsights(String userId, SocialInsightGenerateRequest request);
|
||||
|
||||
List<SocialProfileInsightResponse> listByUser(String userId, String status);
|
||||
|
||||
SocialProfileInsightResponse updateByUser(String userId, String id, SocialInsightUpdateRequest request);
|
||||
|
||||
boolean deleteByUser(String userId, String id);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.dto.request.SystemConfigUpdateRequest;
|
||||
import com.emotion.dto.response.LoginConfigResponse;
|
||||
import com.emotion.dto.response.SystemConfigResponse;
|
||||
import com.emotion.entity.SystemConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统配置服务接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-27
|
||||
*/
|
||||
public interface SystemConfigService extends IService<SystemConfig> {
|
||||
|
||||
/**
|
||||
* 获取所有可见配置列表
|
||||
*/
|
||||
List<SystemConfigResponse> getVisibleConfigList();
|
||||
|
||||
/**
|
||||
* 批量更新配置
|
||||
*/
|
||||
void batchUpdateConfigs(List<SystemConfigUpdateRequest> requests);
|
||||
|
||||
/**
|
||||
* 获取配置值(带 Redis 缓存)
|
||||
*
|
||||
* @param configKey 配置标识
|
||||
* @return 配置值字符串,不存在返回 null
|
||||
*/
|
||||
String getConfigValue(String configKey);
|
||||
|
||||
/**
|
||||
* 获取登录方式配置
|
||||
*/
|
||||
LoginConfigResponse getLoginConfig();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 令牌服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface TokenService {
|
||||
|
||||
/**
|
||||
* 从请求中提取并验证令牌,获取用户信息
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户信息响应
|
||||
*/
|
||||
UserInfoResponse getUserInfoByToken(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 从请求中提取并验证令牌,获取用户名
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户名
|
||||
*/
|
||||
String getUsernameByToken(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 验证令牌并返回用户ID
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户ID
|
||||
*/
|
||||
String validateTokenAndGetUserId(HttpServletRequest request);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.TopicInteractionCreateRequest;
|
||||
import com.emotion.dto.request.TopicInteractionPageRequest;
|
||||
import com.emotion.dto.request.TopicInteractionUpdateRequest;
|
||||
import com.emotion.dto.response.TopicInteractionResponse;
|
||||
import com.emotion.entity.TopicInteraction;
|
||||
|
||||
/**
|
||||
* 话题互动服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface TopicInteractionService extends IService<TopicInteraction> {
|
||||
|
||||
/**
|
||||
* 分页查询话题互动
|
||||
*/
|
||||
PageResult<TopicInteractionResponse> getPageWithResponse(TopicInteractionPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取话题互动响应对象
|
||||
*/
|
||||
TopicInteractionResponse getTopicInteractionResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建话题互动
|
||||
*/
|
||||
TopicInteractionResponse createTopicInteractionWithResponse(TopicInteractionCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新话题互动
|
||||
*/
|
||||
TopicInteractionResponse updateTopicInteractionWithResponse(TopicInteractionUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除话题互动
|
||||
*/
|
||||
boolean deleteTopicInteraction(String id);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.emotion.service;
|
||||
|
||||
public interface TtsEngineClient {
|
||||
|
||||
TtsEngineResult synthesize(String text, String voice, String outputPath, SynthesisOptions options);
|
||||
|
||||
class SynthesisOptions {
|
||||
private final Double speechRate;
|
||||
private final Double pitch;
|
||||
private final String emotion;
|
||||
|
||||
public SynthesisOptions(Double speechRate, Double pitch, String emotion) {
|
||||
this.speechRate = speechRate;
|
||||
this.pitch = pitch;
|
||||
this.emotion = emotion;
|
||||
}
|
||||
|
||||
public Double getSpeechRate() {
|
||||
return speechRate;
|
||||
}
|
||||
|
||||
public Double getPitch() {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
public String getEmotion() {
|
||||
return emotion;
|
||||
}
|
||||
|
||||
public String cacheKey() {
|
||||
return "rate=" + (speechRate == null ? "" : speechRate)
|
||||
+ ";pitch=" + (pitch == null ? "" : pitch)
|
||||
+ ";emotion=" + (emotion == null ? "" : emotion);
|
||||
}
|
||||
}
|
||||
|
||||
class TtsEngineResult {
|
||||
private final boolean success;
|
||||
private final String audioPath;
|
||||
private final Long durationMs;
|
||||
private final String errorMessage;
|
||||
|
||||
public TtsEngineResult(boolean success, String audioPath, Long durationMs, String errorMessage) {
|
||||
this.success = success;
|
||||
this.audioPath = audioPath;
|
||||
this.durationMs = durationMs;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getAudioPath() {
|
||||
return audioPath;
|
||||
}
|
||||
|
||||
public Long getDurationMs() {
|
||||
return durationMs;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.dto.request.tts.TtsTaskCreateRequest;
|
||||
import com.emotion.dto.response.tts.TtsTaskResponse;
|
||||
import com.emotion.entity.TtsTask;
|
||||
|
||||
public interface TtsTaskService extends IService<TtsTask> {
|
||||
|
||||
TtsTaskResponse createOrReuse(TtsTaskCreateRequest request);
|
||||
|
||||
void prewarmEpicScript(String userId, String sourceId);
|
||||
|
||||
TtsTaskResponse getTask(String id);
|
||||
|
||||
TtsTaskResponse getBySource(String sourceType, String sourceId, String voice,
|
||||
Double speechRate, Double pitch, String emotion);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.userprofile.UserProfileCreateRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.response.userprofile.UserProfileResponse;
|
||||
import com.emotion.entity.UserProfile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户档案服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-21
|
||||
*/
|
||||
public interface UserProfileService extends IService<UserProfile> {
|
||||
|
||||
/**
|
||||
* 创建用户档案
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 用户档案响应对象
|
||||
*/
|
||||
UserProfileResponse createProfile(UserProfileCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新用户档案
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 用户档案响应对象
|
||||
*/
|
||||
UserProfileResponse updateProfile(UserProfileUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 获取用户档案详情
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @return 用户档案响应对象
|
||||
*/
|
||||
UserProfileResponse getProfileById(String id);
|
||||
|
||||
/**
|
||||
* 获取当前用户档案详情
|
||||
*
|
||||
* @return 用户档案响应对象
|
||||
*/
|
||||
UserProfileResponse getCurrentUserProfile();
|
||||
|
||||
/**
|
||||
* 分页查询用户档案
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<UserProfileResponse> getProfilePage(UserProfilePageRequest request);
|
||||
|
||||
/**
|
||||
* 列表查询用户档案
|
||||
*
|
||||
* @param request 查询请求
|
||||
* @return 列表结果
|
||||
*/
|
||||
List<UserProfileResponse> getProfileList(UserProfilePageRequest request);
|
||||
|
||||
/**
|
||||
* 删除用户档案
|
||||
*
|
||||
* @param id 档案ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteProfile(String id);
|
||||
|
||||
/**
|
||||
* 迁移用户档案中的生命事件数据到t_life_event表
|
||||
* 将childhood、peak、valley数据迁移到生命事件表
|
||||
*
|
||||
* @return 迁移的记录数
|
||||
*/
|
||||
int migrateLifeEventsFromProfiles();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.UserCreateRequest;
|
||||
import com.emotion.dto.request.UserPageRequest;
|
||||
import com.emotion.dto.request.UserUpdateRequest;
|
||||
import com.emotion.dto.request.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.response.UserResponse;
|
||||
import com.emotion.entity.User;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface UserService extends IService<User> {
|
||||
|
||||
/**
|
||||
* 分页查询用户响应
|
||||
*/
|
||||
PageResult<UserResponse> getPageWithResponse(UserPageRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取用户响应
|
||||
*/
|
||||
UserResponse getUserResponseById(String id);
|
||||
|
||||
/**
|
||||
* 创建用户并返回响应
|
||||
*/
|
||||
UserResponse createUserWithResponse(UserCreateRequest request);
|
||||
|
||||
/**
|
||||
* 更新用户并返回响应
|
||||
*/
|
||||
UserResponse updateUserWithResponse(UserUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 更新当前用户个人资料并返回响应
|
||||
*/
|
||||
UserResponse updateCurrentUserProfileWithResponse(UserProfileUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户个人资料并返回响应
|
||||
*/
|
||||
UserResponse getCurrentUserProfileWithResponse();
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
boolean deleteUser(String id);
|
||||
|
||||
/**
|
||||
* 根据账号获取用户
|
||||
*/
|
||||
User getByAccount(String account);
|
||||
|
||||
/**
|
||||
* 根据邮箱获取用户
|
||||
*/
|
||||
User getByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据手机号获取用户
|
||||
*/
|
||||
User getByPhone(String phone);
|
||||
|
||||
User getByThirdParty(String thirdPartyType, String thirdPartyId);
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*
|
||||
* @param account 账号
|
||||
* @param username 用户名
|
||||
* @param password 密码(明文,会在方法内加密)
|
||||
* @param email 邮箱(可为null)
|
||||
* @param phone 手机号(可为null)
|
||||
* @return 创建的用户
|
||||
*/
|
||||
User createUser(String account, String username, String password, String email, String phone);
|
||||
|
||||
/**
|
||||
* 更新用户最后活跃时间
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param lastActiveTime 最后活跃时间
|
||||
*/
|
||||
void updateLastActiveTime(String userId, LocalDateTime lastActiveTime);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.UserStatsCreateRequest;
|
||||
import com.emotion.dto.request.UserStatsIncrementRequest;
|
||||
import com.emotion.dto.request.UserStatsPageRequest;
|
||||
import com.emotion.dto.request.UserStatsUpdateValueRequest;
|
||||
import com.emotion.dto.response.UserStatsResponse;
|
||||
import com.emotion.entity.UserStats;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户统计服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface UserStatsService extends IService<UserStats> {
|
||||
|
||||
/**
|
||||
* 分页查询用户统计响应
|
||||
*/
|
||||
PageResult<UserStatsResponse> getPageWithResponse(UserStatsPageRequest request);
|
||||
|
||||
/**
|
||||
* 更新用户统计值
|
||||
*/
|
||||
boolean updateStatsValue(UserStatsUpdateValueRequest request);
|
||||
|
||||
/**
|
||||
* 增加用户统计值
|
||||
*/
|
||||
boolean incrementStatsValue(UserStatsIncrementRequest request);
|
||||
|
||||
/**
|
||||
* 重新计算用户统计
|
||||
*/
|
||||
boolean recalculateUserStats(String userId);
|
||||
|
||||
/**
|
||||
* 重新计算所有用户统计
|
||||
*/
|
||||
boolean recalculateAllUserStats();
|
||||
|
||||
/**
|
||||
* 创建或更新用户统计响应
|
||||
*/
|
||||
UserStatsResponse createOrUpdateUserStatsWithResponse(UserStatsCreateRequest request);
|
||||
|
||||
/**
|
||||
* 删除过期的统计数据
|
||||
*/
|
||||
boolean deleteExpiredStats(Integer days);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.WebSocketRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* WebSocket服务接口
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
public interface WebSocketService {
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*
|
||||
* @param request WebSocket请求对象
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleChatMessage(WebSocketRequest request, String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
*
|
||||
* @param request 连接请求对象
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleUserConnect(ConnectRequest request, String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleUserDisconnect(String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleHeartbeat(String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*
|
||||
* @return 在线用户数量
|
||||
*/
|
||||
int getOnlineUserCount();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
|
||||
public interface WechatMiniProgramService {
|
||||
|
||||
WechatCodeSessionResponse code2Session(String code);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.service.ai;
|
||||
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface AiProviderAdapter {
|
||||
|
||||
boolean supports(String providerType);
|
||||
|
||||
void stream(AiProvider provider, AiEndpointConfig endpoint, AiRuntimeRequest request, Consumer<AiStreamEvent> consumer);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.emotion.service.ai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class AiTemplateRenderer {
|
||||
|
||||
public Map<String, Object> mergeInputs(String defaultInputs, Map<String, Object> runtimeInputs) {
|
||||
Map<String, Object> inputs = new HashMap<>();
|
||||
if (StringUtils.hasText(defaultInputs)) {
|
||||
try {
|
||||
JSONObject parsed = JSON.parseObject(defaultInputs);
|
||||
parsed.forEach((key, value) -> {
|
||||
// 兼容 _meta 格式:{ "_meta": {...}, "value": "..." }
|
||||
if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|
||||
inputs.put(key, ((JSONObject) value).get("value"));
|
||||
} else {
|
||||
inputs.put(key, value);
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
inputs.put("default_input", defaultInputs);
|
||||
}
|
||||
}
|
||||
if (runtimeInputs != null) {
|
||||
inputs.putAll(runtimeInputs);
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
public JSONObject renderObject(String template, Map<String, Object> inputs) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return new JSONObject();
|
||||
}
|
||||
String rendered = template;
|
||||
for (Map.Entry<String, Object> entry : inputs.entrySet()) {
|
||||
String value = entry.getValue() == null ? "" : String.valueOf(entry.getValue());
|
||||
rendered = rendered.replace("{{" + entry.getKey() + "}}", value);
|
||||
}
|
||||
return JSON.parseObject(rendered);
|
||||
}
|
||||
|
||||
public String firstText(Map<String, Object> inputs) {
|
||||
for (String key : new String[]{"prompt", "message", "query", "text", "input"}) {
|
||||
Object value = inputs.get(key);
|
||||
if (value != null && StringUtils.hasText(String.valueOf(value))) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.emotion.service.ai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Component
|
||||
public class CozeProviderAdapter implements AiProviderAdapter {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final AiTemplateRenderer templateRenderer;
|
||||
private final ProviderHttpSupport httpSupport;
|
||||
|
||||
public CozeProviderAdapter(RestTemplate restTemplate, AiTemplateRenderer templateRenderer, ProviderHttpSupport httpSupport) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.httpSupport = httpSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String providerType) {
|
||||
return "coze".equalsIgnoreCase(providerType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stream(AiProvider provider, AiEndpointConfig endpoint, AiRuntimeRequest request, Consumer<AiStreamEvent> consumer) {
|
||||
Map<String, Object> inputs = templateRenderer.mergeInputs(endpoint.getDefaultInputs(), request.getInputs());
|
||||
JSONObject body = buildRequestBody(endpoint, request, inputs);
|
||||
String path = StringUtils.hasText(endpoint.getApiPath()) ? endpoint.getApiPath() : defaultPath(endpoint);
|
||||
String url = httpSupport.joinUrl(provider.getBaseUrl(), path);
|
||||
ProviderHttpSupport.Counter counter = new ProviderHttpSupport.Counter();
|
||||
|
||||
restTemplate.execute(url, HttpMethod.POST, clientRequest -> {
|
||||
HttpHeaders headers = clientRequest.getHeaders();
|
||||
httpSupport.applyHeaders(headers, provider, endpoint);
|
||||
clientRequest.getBody().write(JSON.toJSONString(body).getBytes(StandardCharsets.UTF_8));
|
||||
}, response -> {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.startsWith("data:")) {
|
||||
httpSupport.emitSseData(trimmed.substring(5).trim(), consumer, this::extractCozeDelta, counter);
|
||||
} else if (trimmed.startsWith("{")) {
|
||||
httpSupport.emitSseData(trimmed, consumer, this::extractCozeDelta, counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (counter.get() == 0) {
|
||||
throw new IllegalStateException("AI_STREAM_NO_DELTA");
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject buildRequestBody(AiEndpointConfig endpoint, AiRuntimeRequest request, Map<String, Object> inputs) {
|
||||
if (StringUtils.hasText(endpoint.getRequestTemplate())) {
|
||||
JSONObject rendered = templateRenderer.renderObject(endpoint.getRequestTemplate(), inputs);
|
||||
rendered.putIfAbsent("stream", true);
|
||||
return rendered;
|
||||
}
|
||||
|
||||
JSONObject body = new JSONObject();
|
||||
if ("workflow".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
body.put("workflow_id", endpoint.getWorkflowId());
|
||||
if (StringUtils.hasText(endpoint.getBotId())) {
|
||||
body.put("bot_id", endpoint.getBotId());
|
||||
}
|
||||
body.put("user_id", StringUtils.hasText(request.getUserId()) ? request.getUserId() : "anonymous");
|
||||
body.put("stream", true);
|
||||
body.put("parameters", inputs);
|
||||
return body;
|
||||
}
|
||||
|
||||
body.put("bot_id", endpoint.getBotId());
|
||||
body.put("user_id", StringUtils.hasText(request.getUserId()) ? request.getUserId() : "anonymous");
|
||||
body.put("stream", true);
|
||||
body.put("auto_save_history", false);
|
||||
JSONArray messages = new JSONArray();
|
||||
JSONObject message = new JSONObject();
|
||||
message.put("role", "user");
|
||||
message.put("content_type", "text");
|
||||
message.put("content", templateRenderer.firstText(inputs));
|
||||
message.put("type", "question");
|
||||
messages.add(message);
|
||||
body.put("additional_messages", messages);
|
||||
body.put("parameters", new JSONObject());
|
||||
if (StringUtils.hasText(endpoint.getWorkflowId())) {
|
||||
body.put("workflow_id", endpoint.getWorkflowId());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private String defaultPath(AiEndpointConfig endpoint) {
|
||||
if ("workflow".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
return "/v1/workflow/stream_run";
|
||||
}
|
||||
return "/v3/chat";
|
||||
}
|
||||
|
||||
private String extractCozeDelta(JSONObject json) {
|
||||
String type = json.getString("type");
|
||||
String content = json.getString("content");
|
||||
if (StringUtils.hasText(content) && (type == null || type.contains("answer") || type.contains("delta"))) {
|
||||
return normalizeOutputText(content);
|
||||
}
|
||||
String output = json.getString("output");
|
||||
if (StringUtils.hasText(output)) {
|
||||
return normalizeOutputText(output);
|
||||
}
|
||||
String answer = json.getString("answer");
|
||||
if (StringUtils.hasText(answer)) {
|
||||
return normalizeOutputText(answer);
|
||||
}
|
||||
JSONObject message = json.getJSONObject("message");
|
||||
if (message != null && StringUtils.hasText(message.getString("content"))) {
|
||||
return normalizeOutputText(message.getString("content"));
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String dataOutput = data.getString("output");
|
||||
if (StringUtils.hasText(dataOutput)) {
|
||||
return normalizeOutputText(dataOutput);
|
||||
}
|
||||
String dataAnswer = data.getString("answer");
|
||||
if (StringUtils.hasText(dataAnswer)) {
|
||||
return normalizeOutputText(dataAnswer);
|
||||
}
|
||||
String dataContent = data.getString("content");
|
||||
if (StringUtils.hasText(dataContent)) {
|
||||
return normalizeOutputText(dataContent);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeOutputText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(trimmed);
|
||||
String extracted = firstText(json);
|
||||
return StringUtils.hasText(extracted) ? normalizeOutputText(extracted) : value;
|
||||
} catch (Exception ignored) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private String firstText(JSONObject json) {
|
||||
String direct = firstTextValue(json, "output", "answer", "content", "text", "result");
|
||||
if (StringUtils.hasText(direct)) {
|
||||
return direct;
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String dataText = firstText(data);
|
||||
if (StringUtils.hasText(dataText)) {
|
||||
return dataText;
|
||||
}
|
||||
}
|
||||
JSONObject outputs = json.getJSONObject("outputs");
|
||||
if (outputs != null) {
|
||||
return firstText(outputs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String firstTextValue(JSONObject json, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = json.get(key);
|
||||
if (value instanceof String text && StringUtils.hasText(text)) {
|
||||
return text;
|
||||
}
|
||||
if (value instanceof JSONObject object) {
|
||||
String nested = firstText(object);
|
||||
if (StringUtils.hasText(nested)) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.emotion.service.ai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Component
|
||||
public class DifyProviderAdapter implements AiProviderAdapter {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final AiTemplateRenderer templateRenderer;
|
||||
private final ProviderHttpSupport httpSupport;
|
||||
|
||||
public DifyProviderAdapter(RestTemplate restTemplate, AiTemplateRenderer templateRenderer, ProviderHttpSupport httpSupport) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.httpSupport = httpSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String providerType) {
|
||||
return "dify".equalsIgnoreCase(providerType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stream(AiProvider provider, AiEndpointConfig endpoint, AiRuntimeRequest request, Consumer<AiStreamEvent> consumer) {
|
||||
Map<String, Object> inputs = templateRenderer.mergeInputs(endpoint.getDefaultInputs(), request.getInputs());
|
||||
JSONObject body = buildRequestBody(endpoint, request, inputs);
|
||||
String path = StringUtils.hasText(endpoint.getApiPath()) ? endpoint.getApiPath() : defaultPath(endpoint);
|
||||
String url = httpSupport.joinUrl(provider.getBaseUrl(), path);
|
||||
ProviderHttpSupport.Counter counter = new ProviderHttpSupport.Counter();
|
||||
|
||||
restTemplate.execute(url, HttpMethod.POST, clientRequest -> {
|
||||
HttpHeaders headers = clientRequest.getHeaders();
|
||||
httpSupport.applyHeaders(headers, provider, endpoint);
|
||||
clientRequest.getBody().write(JSON.toJSONString(body).getBytes(StandardCharsets.UTF_8));
|
||||
}, response -> {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.startsWith("data:")) {
|
||||
httpSupport.emitSseData(trimmed.substring(5).trim(), consumer, this::extractDifyDelta, counter);
|
||||
} else if (trimmed.startsWith("{")) {
|
||||
httpSupport.emitSseData(trimmed, consumer, this::extractDifyDelta, counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (counter.get() == 0) {
|
||||
throw new IllegalStateException("AI_STREAM_NO_DELTA");
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject buildRequestBody(AiEndpointConfig endpoint, AiRuntimeRequest request, Map<String, Object> inputs) {
|
||||
if (StringUtils.hasText(endpoint.getRequestTemplate())) {
|
||||
JSONObject rendered = templateRenderer.renderObject(endpoint.getRequestTemplate(), inputs);
|
||||
rendered.putIfAbsent("response_mode", "streaming");
|
||||
rendered.putIfAbsent("user", user(request));
|
||||
return rendered;
|
||||
}
|
||||
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("response_mode", "streaming");
|
||||
body.put("user", user(request));
|
||||
Object conversationId = inputs.get("conversation_id");
|
||||
if (conversationId != null && StringUtils.hasText(String.valueOf(conversationId))) {
|
||||
body.put("conversation_id", conversationId);
|
||||
}
|
||||
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
body.put("query", templateRenderer.firstText(inputs));
|
||||
body.put("inputs", inputs);
|
||||
} else {
|
||||
body.put("inputs", inputs);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private String defaultPath(AiEndpointConfig endpoint) {
|
||||
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
return "/chat-messages";
|
||||
}
|
||||
return "/workflows/run";
|
||||
}
|
||||
|
||||
private String user(AiRuntimeRequest request) {
|
||||
return StringUtils.hasText(request.getUserId()) ? request.getUserId() : "anonymous";
|
||||
}
|
||||
|
||||
private String extractDifyDelta(JSONObject json) {
|
||||
String event = json.getString("event");
|
||||
if ("message".equals(event) || "agent_message".equals(event)) {
|
||||
return normalizeOutputText(json.getString("answer"));
|
||||
}
|
||||
if ("text_chunk".equals(event)) {
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
return data == null ? null : normalizeOutputText(data.getString("text"));
|
||||
}
|
||||
if ("workflow_finished".equals(event)) {
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
JSONObject outputs = data == null ? null : data.getJSONObject("outputs");
|
||||
if (outputs != null) {
|
||||
Object text = outputs.get("text");
|
||||
if (text == null) {
|
||||
text = outputs.get("answer");
|
||||
}
|
||||
return text == null ? null : normalizeOutputText(String.valueOf(text));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeOutputText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(trimmed);
|
||||
String extracted = firstText(json);
|
||||
return StringUtils.hasText(extracted) ? normalizeOutputText(extracted) : value;
|
||||
} catch (Exception ignored) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private String firstText(JSONObject json) {
|
||||
String direct = firstTextValue(json, "output", "answer", "content", "text", "result");
|
||||
if (StringUtils.hasText(direct)) {
|
||||
return direct;
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String dataText = firstText(data);
|
||||
if (StringUtils.hasText(dataText)) {
|
||||
return dataText;
|
||||
}
|
||||
}
|
||||
JSONObject outputs = json.getJSONObject("outputs");
|
||||
if (outputs != null) {
|
||||
return firstText(outputs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String firstTextValue(JSONObject json, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = json.get(key);
|
||||
if (value instanceof String text && StringUtils.hasText(text)) {
|
||||
return text;
|
||||
}
|
||||
if (value instanceof JSONObject object) {
|
||||
String nested = firstText(object);
|
||||
if (StringUtils.hasText(nested)) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.emotion.service.ai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Component
|
||||
public class ProviderHttpSupport {
|
||||
|
||||
public String joinUrl(String baseUrl, String path) {
|
||||
String base = StringUtils.hasText(baseUrl) ? baseUrl.trim() : "";
|
||||
String suffix = StringUtils.hasText(path) ? path.trim() : "";
|
||||
if (!base.endsWith("/") && !suffix.startsWith("/")) {
|
||||
return base + "/" + suffix;
|
||||
}
|
||||
if (base.endsWith("/") && suffix.startsWith("/")) {
|
||||
return base + suffix.substring(1);
|
||||
}
|
||||
return base + suffix;
|
||||
}
|
||||
|
||||
public void applyHeaders(HttpHeaders headers, AiProvider provider, AiEndpointConfig endpoint) {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(List.of(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON));
|
||||
if (StringUtils.hasText(provider.getApiKey())) {
|
||||
headers.setBearerAuth(provider.getApiKey());
|
||||
}
|
||||
applyJsonHeaders(headers, provider.getDefaultHeaders());
|
||||
applyJsonHeaders(headers, endpoint.getCustomHeaders());
|
||||
}
|
||||
|
||||
public void emitSseData(String data, Consumer<AiStreamEvent> consumer, DeltaExtractor extractor, Counter counter) {
|
||||
if (!StringUtils.hasText(data) || "[DONE]".equals(data.trim())) {
|
||||
return;
|
||||
}
|
||||
JSONObject json;
|
||||
try {
|
||||
json = JSON.parseObject(data);
|
||||
} catch (Exception ignored) {
|
||||
counter.increment();
|
||||
consumer.accept(AiStreamEvent.delta(data, counter.get()));
|
||||
return;
|
||||
}
|
||||
|
||||
String event = json.getString("event");
|
||||
String type = json.getString("type");
|
||||
if ("error".equalsIgnoreCase(event) || "error".equalsIgnoreCase(type)) {
|
||||
String message = firstText(json, "message", "error", "error_msg", "errorMessage");
|
||||
if (!StringUtils.hasText(message)) {
|
||||
message = data;
|
||||
}
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
try {
|
||||
String delta = extractor.extract(json);
|
||||
if (StringUtils.hasText(delta)) {
|
||||
counter.increment();
|
||||
consumer.accept(AiStreamEvent.delta(delta, counter.get()));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
counter.increment();
|
||||
consumer.accept(AiStreamEvent.delta(data, counter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
private String firstText(JSONObject json, String... keys) {
|
||||
for (String key : keys) {
|
||||
String value = json.getString(key);
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
for (String key : keys) {
|
||||
String value = data.getString(key);
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyJsonHeaders(HttpHeaders headers, String jsonText) {
|
||||
if (!StringUtils.hasText(jsonText)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(jsonText);
|
||||
for (Map.Entry<String, Object> entry : json.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
headers.set(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeltaExtractor {
|
||||
String extract(JSONObject json);
|
||||
}
|
||||
|
||||
public static class Counter {
|
||||
private int value;
|
||||
|
||||
public void increment() {
|
||||
value++;
|
||||
}
|
||||
|
||||
public int get() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.emotion.service.analytics;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AnalyticsDictionary {
|
||||
|
||||
private static final Map<String, String> EVENT_LABELS = mapOf(
|
||||
"app_launch", "启动小程序",
|
||||
"app_show", "打开小程序",
|
||||
"app_hide", "离开小程序",
|
||||
"page_view", "浏览页面",
|
||||
"page_leave", "离开页面",
|
||||
"script_home_view", "进入爽文生成页",
|
||||
"script_my_scripts_click", "点击我的剧本",
|
||||
"script_social_insights_click", "点击人生素材画像",
|
||||
"script_social_import_entry_click", "点击导入社交数据",
|
||||
"script_inspiration_select", "选择灵感推荐",
|
||||
"script_inspiration_refresh", "刷新灵感推荐",
|
||||
"script_voice_press_start", "开始按住说话",
|
||||
"script_voice_press_end", "结束语音输入",
|
||||
"script_voice_record_cancel", "取消语音输入",
|
||||
"script_voice_recognize_success", "语音识别成功",
|
||||
"script_voice_recognize_fail", "语音识别失败",
|
||||
"script_wish_submit", "提交创作愿望",
|
||||
"script_generation_progress_view", "查看生成进度",
|
||||
"script_generate_success", "剧本生成成功",
|
||||
"script_generate_fail", "剧本生成失败",
|
||||
"script_generate_stream_fail", "流式生成失败",
|
||||
"script_result_view", "查看生成结果",
|
||||
"script_result_change_direction_click", "点击换个方向",
|
||||
"script_result_not_like_me_click", "点击不像我",
|
||||
"script_result_tts_click", "点击朗读结果",
|
||||
"script_detail_view", "查看剧本详情",
|
||||
"script_detail_tts_click", "点击详情朗读",
|
||||
"script_tts_request", "请求生成朗读",
|
||||
"script_tts_success", "朗读任务成功",
|
||||
"script_tts_error", "朗读任务失败",
|
||||
"script_tts_play", "播放朗读",
|
||||
"script_tts_pause", "暂停朗读",
|
||||
"script_tts_complete", "朗读播放完成",
|
||||
"path_select", "选择实现路径",
|
||||
"life_event_ai_assist", "点击 AI 辅助记录",
|
||||
"life_event_create", "创建人生事件",
|
||||
"life_event_update", "更新人生事件",
|
||||
"life_event_favorite", "收藏人生事件",
|
||||
"life_event_share", "分享人生事件",
|
||||
"social_import_insights_click", "查看社交素材画像",
|
||||
"social_import_manual_start", "开始手动导入",
|
||||
"social_import_link_start", "开始链接导入",
|
||||
"social_import_screenshot_start", "开始截图导入",
|
||||
"social_import_submit_success", "社交数据导入成功",
|
||||
"social_import_submit_fail", "社交数据导入失败",
|
||||
"social_insights_view", "查看人生素材画像",
|
||||
"social_insight_status_update", "更新素材状态",
|
||||
"api_request_success", "接口调用成功",
|
||||
"api_request_fail", "接口调用失败"
|
||||
);
|
||||
|
||||
private static final Map<String, String> EVENT_TYPE_LABELS = mapOf(
|
||||
"app", "小程序",
|
||||
"page", "页面行为",
|
||||
"script", "剧本创作",
|
||||
"tts", "朗读功能",
|
||||
"life_event", "人生事件",
|
||||
"social_import", "社交数据",
|
||||
"api", "接口调用",
|
||||
"custom", "自定义行为"
|
||||
);
|
||||
|
||||
private static final Map<String, String> PAGE_LABELS = mapOf(
|
||||
"/pages/splash/index", "启动页",
|
||||
"/pages/login/index", "登录页",
|
||||
"/pages/onboarding/index", "引导页",
|
||||
"/pages/main/index", "首页",
|
||||
"/pages/main/ScriptView", "爽文生成页",
|
||||
"/pages/main/RecordView", "人生轨迹页",
|
||||
"/pages/main/MineView", "我的页面",
|
||||
"/pages/main/ScriptDetailView", "剧本详情页",
|
||||
"/pages/main/PathView", "实现路径页",
|
||||
"/pages/life-event/form", "记录人生经历页",
|
||||
"/pages/life-event/detail", "人生事件详情页",
|
||||
"/pages/profile/index", "个人中心页",
|
||||
"/pages/social-import/index", "社交数据导入页",
|
||||
"/pages/social-import/preview", "导入预览页",
|
||||
"/pages/social-import/insights", "人生素材画像页"
|
||||
);
|
||||
|
||||
private static final Map<String, String> DIMENSION_LABELS = mapOf(
|
||||
"style", "剧本风格",
|
||||
"length", "篇幅",
|
||||
"source", "来源",
|
||||
"tab", "所在栏目",
|
||||
"platform", "平台",
|
||||
"status", "状态",
|
||||
"api_path", "接口"
|
||||
);
|
||||
|
||||
private static final Map<String, String> VALUE_LABELS = mapOf(
|
||||
"script", "爽文生成",
|
||||
"record", "人生轨迹",
|
||||
"mine", "我的",
|
||||
"career", "事业逆袭",
|
||||
"romance", "情感成长",
|
||||
"family", "亲情和解",
|
||||
"growth", "自我成长",
|
||||
"爽文", "爽文",
|
||||
"short", "短篇",
|
||||
"medium", "中篇",
|
||||
"long", "长篇",
|
||||
"text", "文本输入",
|
||||
"home", "首页",
|
||||
"recommendation", "推荐灵感",
|
||||
"home_head", "首页头部",
|
||||
"manual", "手动导入",
|
||||
"link", "链接导入",
|
||||
"screenshot", "截图导入",
|
||||
"success", "成功",
|
||||
"fail", "失败",
|
||||
"failed", "失败",
|
||||
"pending", "处理中",
|
||||
"enabled", "已启用",
|
||||
"disabled", "已停用"
|
||||
);
|
||||
|
||||
private static final Map<String, String> API_LABELS = mapOf(
|
||||
"/analytics/events/batch", "上报行为埋点",
|
||||
"/ai/runtime/stream", "AI 流式生成",
|
||||
"/ai/runtime/invoke", "AI 通用调用",
|
||||
"/asr/recognize", "语音识别",
|
||||
"/tts/tasks", "创建朗读任务",
|
||||
"/tts/tasks/by-source", "查询内容朗读任务",
|
||||
"/lifeEvent/list", "查询人生事件",
|
||||
"/lifeEvent/detail", "查看人生事件",
|
||||
"/lifeEvent/create", "创建人生事件",
|
||||
"/lifeEvent/update", "更新人生事件",
|
||||
"/lifeEvent/delete", "删除人生事件",
|
||||
"/lifePath/list", "查询实现路径",
|
||||
"/lifePath/create", "创建实现路径",
|
||||
"/lifePath/update", "更新实现路径",
|
||||
"/epic-script/list", "查询我的剧本",
|
||||
"/epic-script/detail", "查看剧本详情",
|
||||
"/epic-script/create", "保存生成剧本",
|
||||
"/epic-script/update", "更新剧本",
|
||||
"/user-profile/create", "创建用户画像",
|
||||
"/user-profile/update", "更新用户画像",
|
||||
"/social-import", "社交数据导入",
|
||||
"/auth/login", "登录",
|
||||
"/auth/refresh", "刷新登录状态"
|
||||
);
|
||||
|
||||
private AnalyticsDictionary() {
|
||||
}
|
||||
|
||||
public static String eventLabel(String eventName) {
|
||||
return label(EVENT_LABELS, eventName);
|
||||
}
|
||||
|
||||
public static String eventTypeLabel(String eventType) {
|
||||
return label(EVENT_TYPE_LABELS, eventType);
|
||||
}
|
||||
|
||||
public static String pageLabel(String pagePath) {
|
||||
if (!StringUtils.hasText(pagePath)) {
|
||||
return "未记录页面";
|
||||
}
|
||||
String normalized = stripQuery(pagePath);
|
||||
return PAGE_LABELS.getOrDefault(normalized, normalized);
|
||||
}
|
||||
|
||||
public static String dimensionLabel(String dimension) {
|
||||
return label(DIMENSION_LABELS, dimension);
|
||||
}
|
||||
|
||||
public static String valueLabel(String dimension, String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return "未记录";
|
||||
}
|
||||
if ("api_path".equals(dimension)) {
|
||||
return apiLabel(value);
|
||||
}
|
||||
return VALUE_LABELS.getOrDefault(value, value);
|
||||
}
|
||||
|
||||
public static String apiLabel(String apiPath) {
|
||||
if (!StringUtils.hasText(apiPath)) {
|
||||
return "未记录接口";
|
||||
}
|
||||
String normalized = stripQuery(apiPath);
|
||||
if (API_LABELS.containsKey(normalized)) {
|
||||
return API_LABELS.get(normalized);
|
||||
}
|
||||
for (Map.Entry<String, String> entry : API_LABELS.entrySet()) {
|
||||
if (normalized.startsWith(entry.getKey() + "/")) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String label(Map<String, String> labels, String key) {
|
||||
if (!StringUtils.hasText(key)) {
|
||||
return "未记录";
|
||||
}
|
||||
return labels.getOrDefault(key, key);
|
||||
}
|
||||
|
||||
private static String stripQuery(String value) {
|
||||
int queryIndex = value.indexOf('?');
|
||||
return queryIndex >= 0 ? value.substring(0, queryIndex) : value;
|
||||
}
|
||||
|
||||
private static Map<String, String> mapOf(String... entries) {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
for (int i = 0; i + 1 < entries.length; i += 2) {
|
||||
map.put(entries[i], entries[i + 1]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.achievement.AchievementCreateRequest;
|
||||
import com.emotion.dto.request.achievement.AchievementPageRequest;
|
||||
import com.emotion.dto.request.achievement.AchievementUpdateRequest;
|
||||
import com.emotion.dto.response.achievement.AchievementResponse;
|
||||
import com.emotion.entity.Achievement;
|
||||
import com.emotion.mapper.AchievementMapper;
|
||||
import com.emotion.service.AchievementService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 成就服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Service
|
||||
public class AchievementServiceImpl extends ServiceImpl<AchievementMapper, Achievement> implements AchievementService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<Achievement> getPage(AchievementPageRequest request) {
|
||||
Page<Achievement> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(Achievement::getTitle, request.getKeyword())
|
||||
.or().like(Achievement::getDescription, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if (StringUtils.hasText(request.getCategory())) {
|
||||
wrapper.eq(Achievement::getCategory, request.getCategory());
|
||||
}
|
||||
|
||||
// 稀有度筛选
|
||||
if (StringUtils.hasText(request.getRarity())) {
|
||||
wrapper.eq(Achievement::getRarity, request.getRarity());
|
||||
}
|
||||
|
||||
// 解锁状态筛选
|
||||
if (request.getUnlocked() != null) {
|
||||
if (request.getUnlocked()) {
|
||||
wrapper.isNotNull(Achievement::getUnlockedTime);
|
||||
} else {
|
||||
wrapper.isNull(Achievement::getUnlockedTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏状态筛选
|
||||
if (request.getHidden() != null) {
|
||||
wrapper.eq(Achievement::getIsHidden, request.getHidden() ? 1 : 0);
|
||||
}
|
||||
|
||||
wrapper.eq(Achievement::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(Achievement::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(Achievement::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(Achievement::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getByCategory(String category) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getCategory, category)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getByRarity(String rarity) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getRarity, rarity)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getByConditionType(String conditionType) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getConditionType, conditionType)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getUnlockedAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNotNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getUnlockedTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getLockedAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getHiddenAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getIsHidden, 1)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getVisibleAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getIsHidden, 0)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getByProgressRange(Double minProgress, Double maxProgress) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(Achievement::getProgress, minProgress, maxProgress)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getProgress);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getByUnlockTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(Achievement::getUnlockedTime, startTime, endTime)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getUnlockedTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countUnlockedAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNotNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countLockedAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByCategory(String category) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getCategory, category)
|
||||
.eq(Achievement::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByRarity(String rarity) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getRarity, rarity)
|
||||
.eq(Achievement::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getAvgProgress() {
|
||||
List<Achievement> achievements = this.list(new LambdaQueryWrapper<Achievement>()
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.isNotNull(Achievement::getProgress));
|
||||
return achievements.stream()
|
||||
.mapToDouble(a -> a.getProgress() != null ? a.getProgress().doubleValue() : 0.0)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getAvgProgressByCategory(String category) {
|
||||
List<Achievement> achievements = this.list(new LambdaQueryWrapper<Achievement>()
|
||||
.eq(Achievement::getCategory, category)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.isNotNull(Achievement::getProgress));
|
||||
return achievements.stream()
|
||||
.mapToDouble(a -> a.getProgress() != null ? a.getProgress().doubleValue() : 0.0)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getRecentlyUnlocked(Integer limit) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNotNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getUnlockedTime)
|
||||
.last("LIMIT " + limit);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getNearCompletion() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.gt(Achievement::getProgress, 80)
|
||||
.isNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getProgress);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getRareAchievements() {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.in(Achievement::getRarity, "legendary", "epic")
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlockAchievement(String id, LocalDateTime unlockedTime) {
|
||||
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Achievement::getId, id)
|
||||
.set(Achievement::getUnlockedTime, unlockedTime)
|
||||
.set(Achievement::getProgress, 100)
|
||||
.set(Achievement::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateProgress(String id, Double progress) {
|
||||
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Achievement::getId, id)
|
||||
.set(Achievement::getProgress, progress)
|
||||
.set(Achievement::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateHiddenStatus(String id, Integer isHidden) {
|
||||
LambdaUpdateWrapper<Achievement> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Achievement::getId, id)
|
||||
.set(Achievement::getIsHidden, isHidden)
|
||||
.set(Achievement::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Achievement> getRecommendedAchievements(String category, String rarity, Integer limit) {
|
||||
LambdaQueryWrapper<Achievement> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Achievement::getCategory, category)
|
||||
.eq(Achievement::getRarity, rarity)
|
||||
.isNull(Achievement::getUnlockedTime)
|
||||
.eq(Achievement::getIsHidden, 0)
|
||||
.eq(Achievement::getIsDeleted, 0)
|
||||
.orderByDesc(Achievement::getCreateTime)
|
||||
.last("LIMIT " + limit);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
// 新增的Response相关方法实现
|
||||
|
||||
@Override
|
||||
public PageResult<AchievementResponse> getPageWithResponse(AchievementPageRequest request) {
|
||||
IPage<Achievement> page = getPage(request);
|
||||
List<AchievementResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<AchievementResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AchievementResponse getAchievementResponseById(String id) {
|
||||
Achievement achievement = this.getById(id);
|
||||
if (achievement == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(achievement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AchievementResponse createAchievementWithResponse(AchievementCreateRequest request) {
|
||||
Achievement achievement = new Achievement();
|
||||
org.springframework.beans.BeanUtils.copyProperties(request, achievement);
|
||||
achievement.setId(snowflakeIdGenerator.nextIdAsString());
|
||||
|
||||
boolean saved = this.save(achievement);
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(achievement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AchievementResponse updateAchievementWithResponse(AchievementUpdateRequest request) {
|
||||
Achievement achievement = this.getById(request.getId());
|
||||
if (achievement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新非空字段
|
||||
if (request.getTitle() != null) {
|
||||
achievement.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getDescription() != null) {
|
||||
achievement.setDescription(request.getDescription());
|
||||
}
|
||||
if (request.getCategory() != null) {
|
||||
achievement.setCategory(request.getCategory());
|
||||
}
|
||||
if (request.getIcon() != null) {
|
||||
achievement.setIcon(request.getIcon());
|
||||
}
|
||||
if (request.getRarity() != null) {
|
||||
achievement.setRarity(request.getRarity());
|
||||
}
|
||||
if (request.getConditionType() != null) {
|
||||
achievement.setConditionType(request.getConditionType());
|
||||
}
|
||||
if (request.getConditionValue() != null) {
|
||||
achievement.setConditionValue(request.getConditionValue());
|
||||
}
|
||||
if (request.getRewards() != null) {
|
||||
achievement.setRewards(request.getRewards());
|
||||
}
|
||||
if (request.getIsHidden() != null) {
|
||||
achievement.setIsHidden(request.getIsHidden());
|
||||
}
|
||||
|
||||
achievement.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
boolean updated = this.updateById(achievement);
|
||||
if (!updated) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(achievement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AchievementResponse> getByCategoryWithResponse(String category) {
|
||||
List<Achievement> achievements = getByCategory(category);
|
||||
return achievements.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AchievementResponse> getByRarityWithResponse(String rarity) {
|
||||
List<Achievement> achievements = getByRarity(rarity);
|
||||
return achievements.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AchievementResponse> getUnlockedAchievementsWithResponse() {
|
||||
List<Achievement> achievements = getUnlockedAchievements();
|
||||
return achievements.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AchievementResponse> getLockedAchievementsWithResponse() {
|
||||
List<Achievement> achievements = getLockedAchievements();
|
||||
return achievements.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AchievementResponse> getRecentlyUnlockedWithResponse(Integer limit) {
|
||||
List<Achievement> achievements = getRecentlyUnlocked(limit);
|
||||
return achievements.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private AchievementResponse convertToResponse(Achievement achievement) {
|
||||
AchievementResponse response = new AchievementResponse();
|
||||
org.springframework.beans.BeanUtils.copyProperties(achievement, response);
|
||||
response.setId(achievement.getId());
|
||||
if (achievement.getCreateTime() != null) {
|
||||
response.setCreateTime(achievement.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (achievement.getUpdateTime() != null) {
|
||||
response.setUpdateTime(achievement.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (achievement.getUnlockedTime() != null) {
|
||||
response.setUnlockedTime(achievement.getUnlockedTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.dto.request.AdminChangePasswordRequest;
|
||||
import com.emotion.dto.request.AdminLoginRequest;
|
||||
import com.emotion.dto.response.AdminAuthResponse;
|
||||
import com.emotion.dto.response.AdminInfoResponse;
|
||||
import com.emotion.entity.Admin;
|
||||
import com.emotion.exception.AuthException;
|
||||
import com.emotion.service.AdminAuthService;
|
||||
import com.emotion.service.AdminService;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 管理员认证服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AdminAuthServiceImpl implements AdminAuthService {
|
||||
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String ADMIN_TOKEN_PREFIX = "admin_token:";
|
||||
private static final String ADMIN_REFRESH_TOKEN_PREFIX = "admin_refresh_token:";
|
||||
private static final int TOKEN_EXPIRE_HOURS = 24;
|
||||
private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public AdminAuthResponse login(AdminLoginRequest request) {
|
||||
Admin admin = adminService.getByAccount(request.getAccount());
|
||||
|
||||
if (admin == null) {
|
||||
throw new AuthException("账号或密码错误");
|
||||
}
|
||||
|
||||
if (admin.getStatus() != 1) {
|
||||
throw new AuthException("账号已被禁用");
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(request.getPassword(), admin.getPassword())) {
|
||||
throw new AuthException("账号或密码错误");
|
||||
}
|
||||
|
||||
log.info("管理员登录成功: account={}, adminId={}", request.getAccount(), admin.getId());
|
||||
|
||||
String accessToken = generateAccessToken(admin);
|
||||
String refreshToken = generateRefreshToken(admin);
|
||||
|
||||
updateLoginInfo(admin);
|
||||
|
||||
AdminAuthResponse response = new AdminAuthResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setAdminInfo(convertToAdminInfoResponse(admin));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminInfoResponse getCurrentAdminInfo(String adminId) {
|
||||
Admin admin = adminService.getById(adminId);
|
||||
if (admin == null) {
|
||||
throw new AuthException("管理员不存在");
|
||||
}
|
||||
return convertToAdminInfoResponse(admin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
String adminId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
if (adminId != null) {
|
||||
redisTemplate.delete(ADMIN_TOKEN_PREFIX + adminId);
|
||||
log.info("管理员登出成功: adminId={}", adminId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminAuthResponse refreshToken(String refreshToken) {
|
||||
if (!jwtUtil.validateToken(refreshToken)) {
|
||||
throw new AuthException("刷新令牌无效或已过期");
|
||||
}
|
||||
|
||||
String adminId = jwtUtil.getUserIdFromToken(refreshToken);
|
||||
String userType = jwtUtil.getUserTypeFromToken(refreshToken);
|
||||
|
||||
if (!"admin".equals(userType)) {
|
||||
throw new AuthException("无效的刷新令牌");
|
||||
}
|
||||
|
||||
String cachedToken = (String) redisTemplate.opsForValue().get(ADMIN_REFRESH_TOKEN_PREFIX + adminId);
|
||||
if (cachedToken == null || !cachedToken.equals(refreshToken)) {
|
||||
throw new AuthException("刷新令牌已失效");
|
||||
}
|
||||
|
||||
Admin admin = adminService.getById(adminId);
|
||||
if (admin == null) {
|
||||
throw new AuthException("管理员不存在");
|
||||
}
|
||||
|
||||
if (admin.getStatus() != 1) {
|
||||
throw new AuthException("账号已被禁用");
|
||||
}
|
||||
|
||||
String newAccessToken = generateAccessToken(admin);
|
||||
String newRefreshToken = generateRefreshToken(admin);
|
||||
|
||||
AdminAuthResponse response = new AdminAuthResponse();
|
||||
response.setAccessToken(newAccessToken);
|
||||
response.setRefreshToken(newRefreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setAdminInfo(convertToAdminInfoResponse(admin));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
if (!jwtUtil.validateToken(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String userType = jwtUtil.getUserTypeFromToken(token);
|
||||
return "admin".equals(userType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAdminIdFromToken(String token) {
|
||||
return jwtUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
private String generateAccessToken(Admin admin) {
|
||||
String token = jwtUtil.generateToken(admin.getId(), admin.getUsername(), "admin");
|
||||
redisTemplate.opsForValue().set(
|
||||
ADMIN_TOKEN_PREFIX + admin.getId(),
|
||||
token,
|
||||
TOKEN_EXPIRE_HOURS,
|
||||
TimeUnit.HOURS
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
private String generateRefreshToken(Admin admin) {
|
||||
String token = jwtUtil.generateRefreshToken(admin.getId(), admin.getUsername(), "admin");
|
||||
redisTemplate.opsForValue().set(
|
||||
ADMIN_REFRESH_TOKEN_PREFIX + admin.getId(),
|
||||
token,
|
||||
REFRESH_TOKEN_EXPIRE_DAYS,
|
||||
TimeUnit.DAYS
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
private void updateLoginInfo(Admin admin) {
|
||||
admin.setLastLoginTime(LocalDateTime.now());
|
||||
admin.setLoginCount(admin.getLoginCount() == null ? 1 : admin.getLoginCount() + 1);
|
||||
adminService.updateById(admin);
|
||||
}
|
||||
|
||||
private AdminInfoResponse convertToAdminInfoResponse(Admin admin) {
|
||||
AdminInfoResponse response = new AdminInfoResponse();
|
||||
BeanUtils.copyProperties(admin, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(String adminId, AdminChangePasswordRequest request) {
|
||||
Admin admin = adminService.getById(adminId);
|
||||
if (admin == null) {
|
||||
throw new AuthException("管理员不存在");
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(request.getOldPassword(), admin.getPassword())) {
|
||||
throw new AuthException("原密码不正确");
|
||||
}
|
||||
|
||||
admin.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
||||
adminService.updateById(admin);
|
||||
|
||||
// 清除该管理员的Redis token,强制重新登录
|
||||
redisTemplate.delete(ADMIN_TOKEN_PREFIX + adminId);
|
||||
redisTemplate.delete(ADMIN_REFRESH_TOKEN_PREFIX + adminId);
|
||||
|
||||
log.info("管理员修改密码成功: adminId={}", adminId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.AdminCreateRequest;
|
||||
import com.emotion.dto.request.AdminPageRequest;
|
||||
import com.emotion.dto.request.AdminUpdateRequest;
|
||||
import com.emotion.dto.response.AdminResponse;
|
||||
import com.emotion.entity.Admin;
|
||||
import com.emotion.exception.BusinessException;
|
||||
import com.emotion.mapper.AdminMapper;
|
||||
import com.emotion.service.AdminService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 管理员服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public AdminServiceImpl(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AdminResponse> getPageWithResponse(AdminPageRequest request) {
|
||||
Page<Admin> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(Admin::getAccount, request.getKeyword())
|
||||
.or().like(Admin::getUsername, request.getKeyword())
|
||||
.or().like(Admin::getEmail, request.getKeyword())
|
||||
.or().like(Admin::getPhone, request.getKeyword()));
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getAccount())) {
|
||||
wrapper.like(Admin::getAccount, request.getAccount());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getUsername())) {
|
||||
wrapper.like(Admin::getUsername, request.getUsername());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
wrapper.like(Admin::getEmail, request.getEmail());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
wrapper.like(Admin::getPhone, request.getPhone());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getRole())) {
|
||||
wrapper.eq(Admin::getRole, request.getRole());
|
||||
}
|
||||
|
||||
if (request.getStatus() != null) {
|
||||
wrapper.eq(Admin::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getDepartment())) {
|
||||
wrapper.like(Admin::getDepartment, request.getDepartment());
|
||||
}
|
||||
|
||||
wrapper.eq(Admin::getIsDeleted, 0);
|
||||
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(Admin::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(Admin::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(Admin::getCreateTime);
|
||||
}
|
||||
|
||||
IPage<Admin> adminPage = this.page(page, wrapper);
|
||||
|
||||
List<AdminResponse> responseList = adminPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<AdminResponse> result = new PageResult<>();
|
||||
result.setRecords(responseList);
|
||||
result.setTotal(adminPage.getTotal());
|
||||
result.setCurrent(adminPage.getCurrent());
|
||||
result.setSize(adminPage.getSize());
|
||||
result.setPages(adminPage.getPages());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminResponse getAdminResponseById(String id) {
|
||||
Admin admin = this.getById(id);
|
||||
if (admin == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(admin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminResponse createAdminWithResponse(AdminCreateRequest request) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getAccount, request.getAccount())
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException("账号已存在");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getEmail, request.getEmail())
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException("邮箱已存在");
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getPhone, request.getPhone())
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException("手机号已存在");
|
||||
}
|
||||
}
|
||||
|
||||
Admin admin = new Admin();
|
||||
BeanUtils.copyProperties(request, admin);
|
||||
admin.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
admin.setStatus(1);
|
||||
admin.setLoginCount(0);
|
||||
|
||||
boolean saved = this.save(admin);
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToResponse(admin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminResponse updateAdminWithResponse(AdminUpdateRequest request) {
|
||||
Admin admin = this.getById(request.getId());
|
||||
if (admin == null) {
|
||||
throw new BusinessException("管理员不存在");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getEmail()) && !request.getEmail().equals(admin.getEmail())) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getEmail, request.getEmail())
|
||||
.ne(Admin::getId, request.getId())
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException("邮箱已存在");
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getPhone()) && !request.getPhone().equals(admin.getPhone())) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getPhone, request.getPhone())
|
||||
.ne(Admin::getId, request.getId())
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException("手机号已存在");
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getUsername())) {
|
||||
admin.setUsername(request.getUsername());
|
||||
}
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
admin.setEmail(request.getEmail());
|
||||
}
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
admin.setPhone(request.getPhone());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAvatar())) {
|
||||
admin.setAvatar(request.getAvatar());
|
||||
}
|
||||
if (StringUtils.hasText(request.getRole())) {
|
||||
admin.setRole(request.getRole());
|
||||
}
|
||||
if (request.getPermissions() != null) {
|
||||
admin.setPermissions(request.getPermissions());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
admin.setStatus(request.getStatus());
|
||||
}
|
||||
if (StringUtils.hasText(request.getDepartment())) {
|
||||
admin.setDepartment(request.getDepartment());
|
||||
}
|
||||
if (StringUtils.hasText(request.getPosition())) {
|
||||
admin.setPosition(request.getPosition());
|
||||
}
|
||||
|
||||
boolean updated = this.updateById(admin);
|
||||
if (!updated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToResponse(admin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Admin getByAccount(String account) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getAccount, account)
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Admin getByEmail(String email) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getEmail, email)
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Admin getByPhone(String phone) {
|
||||
LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Admin::getPhone, phone)
|
||||
.eq(Admin::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetPassword(String adminId, String newPassword) {
|
||||
Admin admin = this.getById(adminId);
|
||||
if (admin == null) {
|
||||
throw new BusinessException("管理员不存在");
|
||||
}
|
||||
|
||||
admin.setPassword(passwordEncoder.encode(newPassword));
|
||||
this.updateById(admin);
|
||||
|
||||
// 清除该管理员的Redis token,强制重新登录
|
||||
redisTemplate.delete("admin_token:" + adminId);
|
||||
redisTemplate.delete("admin_refresh_token:" + adminId);
|
||||
|
||||
log.info("管理员重置密码成功: adminId={}", adminId);
|
||||
}
|
||||
|
||||
private AdminResponse convertToResponse(Admin admin) {
|
||||
AdminResponse response = new AdminResponse();
|
||||
BeanUtils.copyProperties(admin, response);
|
||||
|
||||
if (admin.getLastLoginTime() != null) {
|
||||
response.setLastLoginTime(admin.getLastLoginTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (admin.getCreateTime() != null) {
|
||||
response.setCreateTime(admin.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (admin.getUpdateTime() != null) {
|
||||
response.setUpdateTime(admin.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.ai.AiCallLogQueryRequest;
|
||||
import com.emotion.entity.AiCallLog;
|
||||
import com.emotion.mapper.AiCallLogMapper;
|
||||
import com.emotion.service.AiCallLogService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AiCallLogServiceImpl extends ServiceImpl<AiCallLogMapper, AiCallLog> implements AiCallLogService {
|
||||
|
||||
@Override
|
||||
public List<AiCallLog> latest(Integer limit) {
|
||||
int size = limit == null ? 50 : Math.max(1, Math.min(limit, 200));
|
||||
return list(new LambdaQueryWrapper<AiCallLog>()
|
||||
.eq(AiCallLog::getIsDeleted, 0)
|
||||
.orderByDesc(AiCallLog::getCreateTime)
|
||||
.last("limit " + size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
|
||||
Page<AiCallLog> pageParam = new Page<>(request.getPageNum(), request.getPageSize());
|
||||
LambdaQueryWrapper<AiCallLog> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
wrapper.eq(AiCallLog::getIsDeleted, 0)
|
||||
.eq(StringUtils.isNotBlank(request.getStatus()), AiCallLog::getStatus, request.getStatus())
|
||||
.eq(StringUtils.isNotBlank(request.getSceneCode()), AiCallLog::getSceneCode, request.getSceneCode())
|
||||
.eq(StringUtils.isNotBlank(request.getProviderCode()), AiCallLog::getProviderCode, request.getProviderCode())
|
||||
.eq(StringUtils.isNotBlank(request.getEndpointCode()), AiCallLog::getEndpointCode, request.getEndpointCode())
|
||||
.ge(request.getStartTime() != null, AiCallLog::getCreateTime, request.getStartTime())
|
||||
.le(request.getEndTime() != null, AiCallLog::getCreateTime, request.getEndTime())
|
||||
.orderByDesc(AiCallLog::getCreateTime);
|
||||
|
||||
if (StringUtils.isNotBlank(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(AiCallLog::getInputText, request.getKeyword())
|
||||
.or()
|
||||
.like(AiCallLog::getOutputText, request.getKeyword()));
|
||||
}
|
||||
|
||||
IPage<AiCallLog> page = page(pageParam, wrapper);
|
||||
return PageResult.of(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiCallLog findByRequestId(String requestId, String userId) {
|
||||
if (StringUtils.isBlank(requestId)) {
|
||||
return null;
|
||||
}
|
||||
return getOne(new LambdaQueryWrapper<AiCallLog>()
|
||||
.eq(AiCallLog::getIsDeleted, 0)
|
||||
.eq(AiCallLog::getRequestId, requestId)
|
||||
.eq(StringUtils.isNotBlank(userId), AiCallLog::getUserId, userId)
|
||||
.orderByDesc(AiCallLog::getCreateTime)
|
||||
.last("limit 1"));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.aiconfig.AiConfigCreateRequest;
|
||||
import com.emotion.dto.request.aiconfig.AiConfigPageRequest;
|
||||
import com.emotion.dto.request.aiconfig.AiConfigTestUpdateRequest;
|
||||
import com.emotion.dto.request.aiconfig.AiConfigUpdateRequest;
|
||||
import com.emotion.dto.response.aiconfig.AiConfigResponse;
|
||||
import com.emotion.entity.AiConfig;
|
||||
import com.emotion.mapper.AiConfigMapper;
|
||||
import com.emotion.service.AiConfigService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI配置服务实现类
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-10-30
|
||||
*/
|
||||
@Service
|
||||
public class AiConfigServiceImpl extends ServiceImpl<AiConfigMapper, AiConfig> implements AiConfigService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<AiConfig> getPage(AiConfigPageRequest request) {
|
||||
Page<AiConfig> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(AiConfig::getConfigName, request.getKeyword())
|
||||
.or().like(AiConfig::getConfigKey, request.getKeyword())
|
||||
.or().like(AiConfig::getDescription, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 配置类型过滤
|
||||
if (StringUtils.hasText(request.getConfigType())) {
|
||||
wrapper.eq(AiConfig::getConfigType, request.getConfigType());
|
||||
}
|
||||
|
||||
// 服务提供商过滤
|
||||
if (StringUtils.hasText(request.getProvider())) {
|
||||
wrapper.eq(AiConfig::getProvider, request.getProvider());
|
||||
}
|
||||
|
||||
// 使用场景过滤
|
||||
if (StringUtils.hasText(request.getUsageScenario())) {
|
||||
wrapper.eq(AiConfig::getUsageScenario, request.getUsageScenario());
|
||||
}
|
||||
|
||||
// 是否启用过滤
|
||||
if (request.getIsEnabled() != null) {
|
||||
wrapper.eq(AiConfig::getIsEnabled, request.getIsEnabled());
|
||||
}
|
||||
|
||||
// 是否为默认配置过滤
|
||||
if (request.getIsDefault() != null) {
|
||||
wrapper.eq(AiConfig::getIsDefault, request.getIsDefault());
|
||||
}
|
||||
|
||||
// 环境过滤
|
||||
if (StringUtils.hasText(request.getEnvironment())) {
|
||||
wrapper.eq(AiConfig::getEnvironment, request.getEnvironment());
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(getLambdaByField(request.getOrderBy()));
|
||||
} else {
|
||||
wrapper.orderByDesc(getLambdaByField(request.getOrderBy()));
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(AiConfig::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiConfigResponse> getPageWithResponse(AiConfigPageRequest request) {
|
||||
IPage<AiConfig> page = getPage(request);
|
||||
List<AiConfigResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<AiConfigResponse> result = new PageResult<>();
|
||||
result.setCurrent(page.getCurrent());
|
||||
result.setSize(page.getSize());
|
||||
result.setTotal(page.getTotal());
|
||||
result.setPages(page.getPages());
|
||||
result.setRecords(responses);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse getAiConfigResponseById(String id) {
|
||||
AiConfig aiConfig = this.getById(id);
|
||||
return aiConfig != null ? convertToResponse(aiConfig) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse createAiConfigWithResponse(AiConfigCreateRequest request) {
|
||||
AiConfig aiConfig = new AiConfig();
|
||||
BeanUtils.copyProperties(request, aiConfig);
|
||||
aiConfig.setId(String.valueOf(snowflakeIdGenerator.nextId()));
|
||||
|
||||
// 处理JSON字段,防止空字符串导致数据库报错
|
||||
if (!StringUtils.hasText(aiConfig.getCustomHeaders())) {
|
||||
aiConfig.setCustomHeaders("{}");
|
||||
}
|
||||
if (!StringUtils.hasText(aiConfig.getCustomParams())) {
|
||||
aiConfig.setCustomParams("{}");
|
||||
}
|
||||
|
||||
boolean saved = this.save(aiConfig);
|
||||
return saved ? convertToResponse(aiConfig) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse updateAiConfigWithResponse(AiConfigUpdateRequest request) {
|
||||
AiConfig aiConfig = this.getById(request.getId());
|
||||
if (aiConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BeanUtils.copyProperties(request, aiConfig);
|
||||
|
||||
// 处理JSON字段,防止空字符串导致数据库报错
|
||||
if (request.getCustomHeaders() != null && !StringUtils.hasText(request.getCustomHeaders())) {
|
||||
aiConfig.setCustomHeaders("{}");
|
||||
}
|
||||
if (request.getCustomParams() != null && !StringUtils.hasText(request.getCustomParams())) {
|
||||
aiConfig.setCustomParams("{}");
|
||||
}
|
||||
|
||||
boolean updated = this.updateById(aiConfig);
|
||||
return updated ? convertToResponse(aiConfig) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getByConfigType(String configType) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getConfigType, configType);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getByConfigTypeWithResponse(String configType) {
|
||||
return getByConfigType(configType).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getByProvider(String provider) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getProvider, provider);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getByProviderWithResponse(String provider) {
|
||||
return getByProvider(provider).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getByUsageScenario(String usageScenario) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getUsageScenario, usageScenario);
|
||||
wrapper.eq(AiConfig::getIsEnabled, 1);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getByUsageScenarioWithResponse(String usageScenario) {
|
||||
return getByUsageScenario(usageScenario).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getByEnvironment(String environment) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getEnvironment, environment);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getByEnvironmentWithResponse(String environment) {
|
||||
return getByEnvironment(environment).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getEnabledConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsEnabled, 1);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getEnabledConfigsWithResponse() {
|
||||
return getEnabledConfigs().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getDisabledConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsEnabled, 0);
|
||||
wrapper.orderByDesc(AiConfig::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getDisabledConfigsWithResponse() {
|
||||
return getDisabledConfigs().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfig> getDefaultConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsDefault, 1);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConfigResponse> getDefaultConfigsWithResponse() {
|
||||
return getDefaultConfigs().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfig getByConfigKey(String configKey) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getConfigKey, configKey);
|
||||
wrapper.eq(AiConfig::getIsEnabled, 1);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse getByConfigKeyWithResponse(String configKey) {
|
||||
AiConfig aiConfig = getByConfigKey(configKey);
|
||||
return aiConfig != null ? convertToResponse(aiConfig) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableConfig(String id) {
|
||||
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(AiConfig::getId, id);
|
||||
wrapper.set(AiConfig::getIsEnabled, 1);
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableConfig(String id) {
|
||||
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(AiConfig::getId, id);
|
||||
wrapper.set(AiConfig::getIsEnabled, 0);
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setAsDefault(String id) {
|
||||
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(AiConfig::getId, id);
|
||||
wrapper.set(AiConfig::getIsDefault, 1);
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unsetDefault(String id) {
|
||||
LambdaUpdateWrapper<AiConfig> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(AiConfig::getId, id);
|
||||
wrapper.set(AiConfig::getIsDefault, 0);
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfig getBestConfig(String usageScenario, String environment) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getUsageScenario, usageScenario);
|
||||
wrapper.eq(AiConfig::getEnvironment, environment);
|
||||
wrapper.eq(AiConfig::getIsEnabled, 1);
|
||||
wrapper.orderByDesc(AiConfig::getIsDefault);
|
||||
wrapper.orderByDesc(AiConfig::getPriority);
|
||||
wrapper.last("LIMIT 1");
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse getBestConfigWithResponse(String usageScenario, String environment) {
|
||||
AiConfig aiConfig = getBestConfig(usageScenario, environment);
|
||||
return aiConfig != null ? convertToResponse(aiConfig) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countEnabledConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsEnabled, 1);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countDisabledConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsEnabled, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countDefaultConfigs() {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getIsDefault, 1);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByConfigType(String configType) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getConfigType, configType);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByProvider(String provider) {
|
||||
LambdaQueryWrapper<AiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiConfig::getProvider, provider);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应对象
|
||||
*/
|
||||
private AiConfigResponse convertToResponse(AiConfig aiConfig) {
|
||||
AiConfigResponse response = new AiConfigResponse();
|
||||
BeanUtils.copyProperties(aiConfig, response);
|
||||
|
||||
// 格式化时间
|
||||
if (aiConfig.getCreateTime() != null) {
|
||||
response.setCreateTime(aiConfig.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (aiConfig.getUpdateTime() != null) {
|
||||
response.setUpdateTime(aiConfig.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
|
||||
// 管理端不需要脱敏,直接返回完整的API Token
|
||||
response.setApiToken(aiConfig.getApiToken());
|
||||
|
||||
// 转换BigDecimal为Double
|
||||
if (aiConfig.getTemperature() != null) {
|
||||
response.setTemperature(aiConfig.getTemperature().doubleValue());
|
||||
}
|
||||
if (aiConfig.getTopP() != null) {
|
||||
response.setTopP(aiConfig.getTopP().doubleValue());
|
||||
}
|
||||
if (aiConfig.getInputPricePer1k() != null) {
|
||||
response.setInputPricePer1k(aiConfig.getInputPricePer1k().doubleValue());
|
||||
}
|
||||
if (aiConfig.getOutputPricePer1k() != null) {
|
||||
response.setOutputPricePer1k(aiConfig.getOutputPricePer1k().doubleValue());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段名获取对应的Lambda表达式
|
||||
*/
|
||||
private com.baomidou.mybatisplus.core.toolkit.support.SFunction<AiConfig, ?> getLambdaByField(String field) {
|
||||
switch (field) {
|
||||
case "configName":
|
||||
return AiConfig::getConfigName;
|
||||
case "configKey":
|
||||
return AiConfig::getConfigKey;
|
||||
case "configType":
|
||||
return AiConfig::getConfigType;
|
||||
case "provider":
|
||||
return AiConfig::getProvider;
|
||||
case "usageScenario":
|
||||
return AiConfig::getUsageScenario;
|
||||
case "priority":
|
||||
return AiConfig::getPriority;
|
||||
case "createTime":
|
||||
return AiConfig::getCreateTime;
|
||||
case "updateTime":
|
||||
return AiConfig::getUpdateTime;
|
||||
default:
|
||||
return AiConfig::getCreateTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigResponse updateFromTestRequest(AiConfigTestUpdateRequest request) {
|
||||
// 查询现有配置
|
||||
AiConfig aiConfig = this.getById(request.getId());
|
||||
if (aiConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (StringUtils.hasText(request.getApiBaseUrl())) {
|
||||
aiConfig.setApiBaseUrl(request.getApiBaseUrl());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getApiToken())) {
|
||||
aiConfig.setApiToken(request.getApiToken());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getClientId())) {
|
||||
aiConfig.setClientId(request.getClientId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getClientSecret())) {
|
||||
aiConfig.setClientSecret(request.getClientSecret());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getGrantType())) {
|
||||
aiConfig.setGrantType(request.getGrantType());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getBotId())) {
|
||||
aiConfig.setBotId(request.getBotId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getWorkflowId())) {
|
||||
aiConfig.setWorkflowId(request.getWorkflowId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getCustomHeaders())) {
|
||||
aiConfig.setCustomHeaders(request.getCustomHeaders());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getCustomParams())) {
|
||||
aiConfig.setCustomParams(request.getCustomParams());
|
||||
}
|
||||
|
||||
if (request.getSupportStream() != null) {
|
||||
aiConfig.setSupportStream(request.getSupportStream());
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
this.updateById(aiConfig);
|
||||
|
||||
// 返回响应对象
|
||||
return convertToResponse(aiConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.mapper.AiEndpointConfigMapper;
|
||||
import com.emotion.service.AiEndpointConfigService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AiEndpointConfigServiceImpl extends ServiceImpl<AiEndpointConfigMapper, AiEndpointConfig> implements AiEndpointConfigService {
|
||||
|
||||
@Override
|
||||
public List<AiEndpointConfig> listVisible() {
|
||||
return list(new LambdaQueryWrapper<AiEndpointConfig>()
|
||||
.eq(AiEndpointConfig::getIsDeleted, 0)
|
||||
.orderByDesc(AiEndpointConfig::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiEndpointConfig saveEndpoint(AiEndpointConfig endpoint) {
|
||||
applyDefaults(endpoint);
|
||||
save(endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiEndpointConfig updateEndpoint(AiEndpointConfig endpoint) {
|
||||
updateById(endpoint);
|
||||
return getById(endpoint.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiEndpointConfig getEnabledById(String id) {
|
||||
AiEndpointConfig endpoint = getById(id);
|
||||
if (endpoint == null || endpoint.getIsEnabled() == null || endpoint.getIsEnabled() != 1) {
|
||||
return null;
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private void applyDefaults(AiEndpointConfig endpoint) {
|
||||
if (!StringUtils.hasText(endpoint.getResponseMode())) {
|
||||
endpoint.setResponseMode("streaming");
|
||||
}
|
||||
if (endpoint.getSupportStream() == null) {
|
||||
endpoint.setSupportStream(1);
|
||||
}
|
||||
if (endpoint.getIsEnabled() == null) {
|
||||
endpoint.setIsEnabled(1);
|
||||
}
|
||||
if (endpoint.getTimeoutMs() == null) {
|
||||
endpoint.setTimeoutMs(60000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import com.emotion.mapper.AiProviderMapper;
|
||||
import com.emotion.service.AiProviderService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AiProviderServiceImpl extends ServiceImpl<AiProviderMapper, AiProvider> implements AiProviderService {
|
||||
|
||||
@Override
|
||||
public List<AiProvider> listVisible() {
|
||||
return list(new LambdaQueryWrapper<AiProvider>()
|
||||
.eq(AiProvider::getIsDeleted, 0)
|
||||
.orderByDesc(AiProvider::getCreateTime))
|
||||
.stream()
|
||||
.map(this::maskSecret)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiProvider saveProvider(AiProvider provider) {
|
||||
if (provider.getIsEnabled() == null) {
|
||||
provider.setIsEnabled(1);
|
||||
}
|
||||
if (!StringUtils.hasText(provider.getAuthType())) {
|
||||
provider.setAuthType("bearer");
|
||||
}
|
||||
if (provider.getTimeoutMs() == null) {
|
||||
provider.setTimeoutMs(60000);
|
||||
}
|
||||
save(provider);
|
||||
return maskSecret(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiProvider updateProvider(AiProvider provider) {
|
||||
if (!StringUtils.hasText(provider.getApiKey()) || "******".equals(provider.getApiKey())) {
|
||||
provider.setApiKey(null);
|
||||
}
|
||||
updateById(provider);
|
||||
return maskSecret(getById(provider.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiProvider getEnabledById(String id) {
|
||||
AiProvider provider = getById(id);
|
||||
if (provider == null || provider.getIsEnabled() == null || provider.getIsEnabled() != 1) {
|
||||
return null;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private AiProvider maskSecret(AiProvider provider) {
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
AiProvider copy = new AiProvider();
|
||||
BeanUtils.copyProperties(provider, copy);
|
||||
if (StringUtils.hasText(copy.getApiKey())) {
|
||||
copy.setApiKey("******");
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiTestTemplateResponse;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiCallLog;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import com.emotion.entity.AiSceneBinding;
|
||||
import com.emotion.service.AiCallLogService;
|
||||
import com.emotion.service.AiEndpointConfigService;
|
||||
import com.emotion.service.AiProviderService;
|
||||
import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.AiSceneBindingService;
|
||||
import com.emotion.service.ScriptContextService;
|
||||
import com.emotion.service.ai.AiProviderAdapter;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiRuntimeServiceImpl implements AiRuntimeService {
|
||||
|
||||
private final AiSceneBindingService sceneBindingService;
|
||||
private final AiEndpointConfigService endpointConfigService;
|
||||
private final AiProviderService providerService;
|
||||
private final AiCallLogService callLogService;
|
||||
private final ScriptContextService scriptContextService;
|
||||
private final List<AiProviderAdapter> adapters;
|
||||
|
||||
public AiRuntimeServiceImpl(AiSceneBindingService sceneBindingService,
|
||||
AiEndpointConfigService endpointConfigService,
|
||||
AiProviderService providerService,
|
||||
AiCallLogService callLogService,
|
||||
ScriptContextService scriptContextService,
|
||||
List<AiProviderAdapter> adapters) {
|
||||
this.sceneBindingService = sceneBindingService;
|
||||
this.endpointConfigService = endpointConfigService;
|
||||
this.providerService = providerService;
|
||||
this.callLogService = callLogService;
|
||||
this.scriptContextService = scriptContextService;
|
||||
this.adapters = adapters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeStream(AiRuntimeRequest request, Consumer<AiStreamEvent> consumer) {
|
||||
String requestId = StringUtils.hasText(request.getRequestId()) ? request.getRequestId() : UUID.randomUUID().toString();
|
||||
request.setRequestId(requestId);
|
||||
enrichInputs(request);
|
||||
long startedAt = System.currentTimeMillis();
|
||||
AtomicLong firstTokenAt = new AtomicLong(0);
|
||||
AtomicInteger chunks = new AtomicInteger(0);
|
||||
StringBuilder output = new StringBuilder();
|
||||
AiCallLog callLog = new AiCallLog();
|
||||
callLog.setRequestId(requestId);
|
||||
callLog.setSceneCode(request.getSceneCode());
|
||||
callLog.setUserId(resolveUserId(request));
|
||||
callLog.setUserName(request.getUserName());
|
||||
callLog.setInputText(JSON.toJSONString(request.getInputs()));
|
||||
callLog.setStatus("running");
|
||||
|
||||
try {
|
||||
RuntimeTarget target = resolveTarget(request);
|
||||
callLog.setProviderCode(target.provider.getProviderCode());
|
||||
callLog.setEndpointCode(target.endpoint.getEndpointCode());
|
||||
callLogService.save(callLog);
|
||||
|
||||
consumer.accept(AiStreamEvent.start(request.getSceneCode()));
|
||||
target.adapter.stream(target.provider, target.endpoint, request, event -> {
|
||||
if ("delta".equals(event.getType())) {
|
||||
chunks.incrementAndGet();
|
||||
if (firstTokenAt.compareAndSet(0, System.currentTimeMillis())) {
|
||||
log.debug("AI first token emitted, scene={}, requestId={}", request.getSceneCode(), requestId);
|
||||
}
|
||||
if (event.getContent() != null) {
|
||||
output.append(event.getContent());
|
||||
}
|
||||
}
|
||||
consumer.accept(event);
|
||||
});
|
||||
|
||||
callLog.setStatus("success");
|
||||
callLog.setOutputText(output.toString());
|
||||
callLog.setStreamChunks(chunks.get());
|
||||
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
|
||||
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
|
||||
callLogService.updateById(callLog);
|
||||
emitDone(consumer, Map.of(
|
||||
"requestId", requestId,
|
||||
"streamChunks", chunks.get(),
|
||||
"durationMs", callLog.getDurationMs()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
String code = normalizeErrorCode(e);
|
||||
callLog.setOutputText(output.toString());
|
||||
callLog.setStreamChunks(chunks.get());
|
||||
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
|
||||
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
|
||||
if (StringUtils.hasText(output.toString())) {
|
||||
callLog.setStatus("success");
|
||||
callLog.setErrorCode(null);
|
||||
callLog.setErrorMessage(null);
|
||||
saveOrUpdateLog(callLog);
|
||||
emitDone(consumer, recoveredDoneMetadata(requestId, chunks.get(), callLog.getDurationMs(), code, e.getMessage()));
|
||||
return;
|
||||
}
|
||||
callLog.setStatus("failed");
|
||||
callLog.setErrorCode(code);
|
||||
callLog.setErrorMessage(e.getMessage());
|
||||
saveOrUpdateLog(callLog);
|
||||
consumer.accept(AiStreamEvent.error(code, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiRuntimeTestResponse test(AiRuntimeRequest request) {
|
||||
long startedAt = System.currentTimeMillis();
|
||||
StringBuilder output = new StringBuilder();
|
||||
AtomicInteger chunks = new AtomicInteger(0);
|
||||
final String[] errorCode = new String[1];
|
||||
final String[] errorMessage = new String[1];
|
||||
|
||||
invokeStream(request, event -> {
|
||||
if ("delta".equals(event.getType()) && event.getContent() != null) {
|
||||
chunks.incrementAndGet();
|
||||
output.append(event.getContent());
|
||||
} else if ("error".equals(event.getType())) {
|
||||
errorCode[0] = event.getCode();
|
||||
errorMessage[0] = event.getMessage();
|
||||
}
|
||||
});
|
||||
|
||||
return AiRuntimeTestResponse.builder()
|
||||
.sceneCode(request.getSceneCode())
|
||||
.status(errorCode[0] == null || StringUtils.hasText(output.toString()) ? "success" : "failed")
|
||||
.output(output.toString())
|
||||
.streamChunks(chunks.get())
|
||||
.durationMs(System.currentTimeMillis() - startedAt)
|
||||
.errorCode(StringUtils.hasText(output.toString()) ? null : errorCode[0])
|
||||
.errorMessage(StringUtils.hasText(output.toString()) ? null : errorMessage[0])
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs) {
|
||||
long startedAt = System.currentTimeMillis();
|
||||
StringBuilder output = new StringBuilder();
|
||||
AtomicInteger chunks = new AtomicInteger(0);
|
||||
final String[] errorCode = new String[1];
|
||||
final String[] errorMessage = new String[1];
|
||||
|
||||
invokeEndpointStream(endpointId, inputs, event -> {
|
||||
if ("delta".equals(event.getType()) && event.getContent() != null) {
|
||||
chunks.incrementAndGet();
|
||||
output.append(event.getContent());
|
||||
} else if ("error".equals(event.getType())) {
|
||||
errorCode[0] = event.getCode();
|
||||
errorMessage[0] = event.getMessage();
|
||||
}
|
||||
});
|
||||
|
||||
return AiRuntimeTestResponse.builder()
|
||||
.sceneCode("")
|
||||
.status(errorCode[0] == null || StringUtils.hasText(output.toString()) ? "success" : "failed")
|
||||
.output(output.toString())
|
||||
.streamChunks(chunks.get())
|
||||
.durationMs(System.currentTimeMillis() - startedAt)
|
||||
.errorCode(StringUtils.hasText(output.toString()) ? null : errorCode[0])
|
||||
.errorMessage(StringUtils.hasText(output.toString()) ? null : errorMessage[0])
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer) {
|
||||
long startedAt = System.currentTimeMillis();
|
||||
AtomicLong firstTokenAt = new AtomicLong(0);
|
||||
AtomicInteger chunks = new AtomicInteger(0);
|
||||
Object inputRequestId = inputs == null ? null : inputs.get("requestId");
|
||||
String requestId = inputRequestId != null && StringUtils.hasText(String.valueOf(inputRequestId))
|
||||
? String.valueOf(inputRequestId)
|
||||
: UUID.randomUUID().toString();
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
AiEndpointConfig endpoint = endpointConfigService.getEnabledById(endpointId);
|
||||
if (endpoint == null) {
|
||||
throw new IllegalStateException("AI_ENDPOINT_DISABLED");
|
||||
}
|
||||
AiProvider provider = providerService.getEnabledById(endpoint.getProviderId());
|
||||
if (provider == null) {
|
||||
throw new IllegalStateException("AI_PROVIDER_DISABLED");
|
||||
}
|
||||
AiProviderAdapter adapter = adapters.stream()
|
||||
.filter(item -> item.supports(provider.getProviderType()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("AI_PROVIDER_ADAPTER_NOT_FOUND"));
|
||||
|
||||
AiRuntimeRequest request = new AiRuntimeRequest();
|
||||
request.setInputs(inputs == null ? new com.alibaba.fastjson2.JSONObject() : new com.alibaba.fastjson2.JSONObject(inputs));
|
||||
request.setEndpointId(endpointId);
|
||||
request.setUserId(resolveUserId(request));
|
||||
request.setUserName(UserContextHolder.getCurrentUsername());
|
||||
request.setUserType(UserContextHolder.getCurrentUserType());
|
||||
request.setRequestId(requestId);
|
||||
enrichInputs(request);
|
||||
|
||||
AiCallLog callLog = new AiCallLog();
|
||||
callLog.setRequestId(requestId);
|
||||
callLog.setEndpointCode(endpoint.getEndpointCode());
|
||||
callLog.setProviderCode(provider.getProviderCode());
|
||||
callLog.setUserId(request.getUserId());
|
||||
callLog.setUserName(request.getUserName());
|
||||
callLog.setInputText(JSON.toJSONString(request.getInputs()));
|
||||
callLog.setStatus("running");
|
||||
callLogService.save(callLog);
|
||||
|
||||
consumer.accept(AiStreamEvent.start(endpoint.getEndpointCode()));
|
||||
try {
|
||||
adapter.stream(provider, endpoint, request, event -> {
|
||||
if ("delta".equals(event.getType())) {
|
||||
chunks.incrementAndGet();
|
||||
if (firstTokenAt.compareAndSet(0, System.currentTimeMillis())) {
|
||||
log.debug("AI first token emitted, endpoint={}, requestId={}", endpoint.getEndpointCode(), requestId);
|
||||
}
|
||||
if (event.getContent() != null) {
|
||||
output.append(event.getContent());
|
||||
}
|
||||
}
|
||||
consumer.accept(event);
|
||||
});
|
||||
|
||||
callLog.setStatus("success");
|
||||
callLog.setOutputText(output.toString());
|
||||
callLog.setStreamChunks(chunks.get());
|
||||
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
|
||||
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
|
||||
callLogService.updateById(callLog);
|
||||
emitDone(consumer, Map.of(
|
||||
"requestId", requestId,
|
||||
"streamChunks", chunks.get(),
|
||||
"durationMs", callLog.getDurationMs()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
String code = normalizeErrorCode(e);
|
||||
callLog.setOutputText(output.toString());
|
||||
callLog.setStreamChunks(chunks.get());
|
||||
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
|
||||
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
|
||||
if (StringUtils.hasText(output.toString())) {
|
||||
callLog.setStatus("success");
|
||||
callLog.setErrorCode(null);
|
||||
callLog.setErrorMessage(null);
|
||||
saveOrUpdateLog(callLog);
|
||||
emitDone(consumer, recoveredDoneMetadata(requestId, chunks.get(), callLog.getDurationMs(), code, e.getMessage()));
|
||||
return;
|
||||
}
|
||||
callLog.setStatus("failed");
|
||||
callLog.setErrorCode(code);
|
||||
callLog.setErrorMessage(e.getMessage());
|
||||
saveOrUpdateLog(callLog);
|
||||
consumer.accept(AiStreamEvent.error(code, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiTestTemplateResponse buildEndpointTestTemplate(String endpointId) {
|
||||
AiEndpointConfig endpoint = endpointConfigService.getById(endpointId);
|
||||
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
AiProvider provider = providerService.getById(endpoint.getProviderId());
|
||||
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
|
||||
}
|
||||
return buildTemplate(null, endpoint, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiTestTemplateResponse buildSceneTestTemplate(String sceneCode) {
|
||||
AiSceneBinding scene = sceneBindingService.resolveScene(sceneCode);
|
||||
if (scene == null) {
|
||||
throw new IllegalArgumentException("AI_SCENE_NOT_BOUND");
|
||||
}
|
||||
AiEndpointConfig endpoint = endpointConfigService.getById(scene.getEndpointId());
|
||||
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
AiProvider provider = providerService.getById(endpoint.getProviderId());
|
||||
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
|
||||
}
|
||||
return buildTemplate(sceneCode, endpoint, provider);
|
||||
}
|
||||
|
||||
private AiTestTemplateResponse buildTemplate(String sceneCode, AiEndpointConfig endpoint, AiProvider provider) {
|
||||
LinkedHashMap<String, Object> inputs = new LinkedHashMap<>();
|
||||
applyDefaultInputs(inputs, endpoint.getDefaultInputs());
|
||||
applyProviderSampleInputs(inputs, sceneCode, endpoint, provider);
|
||||
|
||||
List<AiTestTemplateResponse.ParamField> fields = new ArrayList<>();
|
||||
inputs.forEach((key, value) -> fields.add(AiTestTemplateResponse.ParamField.builder()
|
||||
.name(key)
|
||||
.label(paramLabel(key))
|
||||
.type(paramType(value))
|
||||
.value(value)
|
||||
.required(requiredParam(key, provider.getProviderType()))
|
||||
.placeholder(paramPlaceholder(key))
|
||||
.build()));
|
||||
|
||||
return AiTestTemplateResponse.builder()
|
||||
.sceneCode(sceneCode)
|
||||
.endpointId(endpoint.getId())
|
||||
.endpointCode(endpoint.getEndpointCode())
|
||||
.endpointName(endpoint.getEndpointName())
|
||||
.providerType(provider.getProviderType())
|
||||
.inputs(inputs)
|
||||
.paramFields(fields)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void applyDefaultInputs(LinkedHashMap<String, Object> inputs, String defaultInputs) {
|
||||
if (!StringUtils.hasText(defaultInputs)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject parsed = JSON.parseObject(defaultInputs);
|
||||
parsed.forEach((key, value) -> {
|
||||
if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|
||||
inputs.put(key, ((JSONObject) value).get("value"));
|
||||
} else {
|
||||
inputs.put(key, value);
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyProviderSampleInputs(LinkedHashMap<String, Object> inputs,
|
||||
String sceneCode,
|
||||
AiEndpointConfig endpoint,
|
||||
AiProvider provider) {
|
||||
String providerType = provider.getProviderType();
|
||||
String sample = samplePrompt(sceneCode, endpoint);
|
||||
|
||||
if ("dify".equalsIgnoreCase(providerType)) {
|
||||
inputs.putIfAbsent("query", sample);
|
||||
inputs.putIfAbsent("inputs", Map.of("input", sample, "user_id", "admin-test-user"));
|
||||
inputs.putIfAbsent("response_mode", "streaming");
|
||||
inputs.putIfAbsent("conversation_id", "");
|
||||
inputs.putIfAbsent("user", "admin-test-user");
|
||||
inputs.putIfAbsent("user_id", "admin-test-user");
|
||||
return;
|
||||
}
|
||||
|
||||
inputs.putIfAbsent("input", sample);
|
||||
inputs.putIfAbsent("prompt", sample);
|
||||
inputs.putIfAbsent("message", sample);
|
||||
inputs.putIfAbsent("user_id", "admin-test-user");
|
||||
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
inputs.putIfAbsent("conversationId", "admin-test-conversation");
|
||||
}
|
||||
}
|
||||
|
||||
private String samplePrompt(String sceneCode, AiEndpointConfig endpoint) {
|
||||
String code = StringUtils.hasText(sceneCode) ? sceneCode : endpoint.getEndpointCode();
|
||||
if (code == null) {
|
||||
return "请用一句中文回复测试成功。";
|
||||
}
|
||||
if (code.contains("script") || code.contains("life.generate")) {
|
||||
return "请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。";
|
||||
}
|
||||
if (code.contains("short_story") || code.contains("story")) {
|
||||
return "请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。";
|
||||
}
|
||||
if (code.contains("diary") || code.contains("healing") || code.contains("life")) {
|
||||
return "今天工作压力很大,但傍晚散步时感觉心情慢慢平静下来,请给我一段温和的回应。";
|
||||
}
|
||||
if (code.contains("summary")) {
|
||||
return "用户今天先表达焦虑,随后通过散步和朋友聊天逐渐恢复平静,请生成一段情绪总结。";
|
||||
}
|
||||
if (code.contains("chat")) {
|
||||
return "你好,请用一句中文回复:AI 接口测试成功。";
|
||||
}
|
||||
return "请用一句中文回复测试成功。";
|
||||
}
|
||||
|
||||
private String paramLabel(String key) {
|
||||
Map<String, String> labels = Map.ofEntries(
|
||||
Map.entry("input", "输入内容"),
|
||||
Map.entry("prompt", "提示词"),
|
||||
Map.entry("message", "消息内容"),
|
||||
Map.entry("query", "用户问题"),
|
||||
Map.entry("inputs", "Dify 变量"),
|
||||
Map.entry("response_mode", "响应模式"),
|
||||
Map.entry("conversation_id", "会话 ID"),
|
||||
Map.entry("user", "用户标识"),
|
||||
Map.entry("user_id", "用户标识"),
|
||||
Map.entry("conversationId", "会话 ID")
|
||||
);
|
||||
return labels.getOrDefault(key, key);
|
||||
}
|
||||
|
||||
private String paramType(Object value) {
|
||||
if (value instanceof Number) {
|
||||
return "number";
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return "boolean";
|
||||
}
|
||||
if (value instanceof Map || value instanceof JSONObject) {
|
||||
return "json";
|
||||
}
|
||||
String text = value == null ? "" : String.valueOf(value);
|
||||
return text.length() > 60 ? "textarea" : "string";
|
||||
}
|
||||
|
||||
private Boolean requiredParam(String key, String providerType) {
|
||||
if ("dify".equalsIgnoreCase(providerType)) {
|
||||
return "query".equals(key) || "response_mode".equals(key) || "user".equals(key);
|
||||
}
|
||||
return "input".equals(key) || "user_id".equals(key);
|
||||
}
|
||||
|
||||
private String paramPlaceholder(String key) {
|
||||
if ("inputs".equals(key)) {
|
||||
return "{\"input\":\"测试内容\"}";
|
||||
}
|
||||
return "请输入" + paramLabel(key);
|
||||
}
|
||||
|
||||
private RuntimeTarget resolveTarget(AiRuntimeRequest request) {
|
||||
if (!StringUtils.hasText(request.getSceneCode())) {
|
||||
throw new IllegalArgumentException("AI_SCENE_REQUIRED");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getUserId())) {
|
||||
request.setUserId(resolveUserId(request));
|
||||
}
|
||||
|
||||
AiSceneBinding scene = sceneBindingService.resolveScene(request.getSceneCode());
|
||||
if (scene == null) {
|
||||
throw new IllegalStateException("AI_SCENE_NOT_BOUND");
|
||||
}
|
||||
AiEndpointConfig endpoint = endpointConfigService.getEnabledById(scene.getEndpointId());
|
||||
if (endpoint == null) {
|
||||
throw new IllegalStateException("AI_ENDPOINT_DISABLED");
|
||||
}
|
||||
if (Integer.valueOf(1).equals(scene.getRequiredStream()) && !Integer.valueOf(1).equals(endpoint.getSupportStream())) {
|
||||
throw new IllegalStateException("AI_ENDPOINT_STREAM_REQUIRED");
|
||||
}
|
||||
AiProvider provider = providerService.getEnabledById(endpoint.getProviderId());
|
||||
if (provider == null) {
|
||||
throw new IllegalStateException("AI_PROVIDER_DISABLED");
|
||||
}
|
||||
AiProviderAdapter adapter = adapters.stream()
|
||||
.filter(item -> item.supports(provider.getProviderType()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("AI_PROVIDER_ADAPTER_NOT_FOUND"));
|
||||
|
||||
return new RuntimeTarget(scene, endpoint, provider, adapter);
|
||||
}
|
||||
|
||||
private String resolveUserId(AiRuntimeRequest request) {
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
return request.getUserId();
|
||||
}
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
return StringUtils.hasText(currentUserId) ? currentUserId : "anonymous";
|
||||
}
|
||||
|
||||
private void enrichInputs(AiRuntimeRequest request) {
|
||||
if (request.getInputs() == null) {
|
||||
request.setInputs(new com.alibaba.fastjson2.JSONObject());
|
||||
}
|
||||
String userId = resolveUserId(request);
|
||||
request.setUserId(userId);
|
||||
request.getInputs().put("userId", userId);
|
||||
request.getInputs().put("currentUserId", userId);
|
||||
request.getInputs().put("user_id", userId);
|
||||
if (StringUtils.hasText(request.getUserName())) {
|
||||
request.getInputs().put("userName", request.getUserName());
|
||||
request.getInputs().put("username", request.getUserName());
|
||||
}
|
||||
if (StringUtils.hasText(request.getUserType())) {
|
||||
request.getInputs().put("userType", request.getUserType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getRequestId())) {
|
||||
request.getInputs().put("requestId", request.getRequestId());
|
||||
}
|
||||
enrichSceneInputs(request, userId);
|
||||
}
|
||||
|
||||
private void enrichSceneInputs(AiRuntimeRequest request, String userId) {
|
||||
if (!StringUtils.hasText(request.getSceneCode())) {
|
||||
return;
|
||||
}
|
||||
if ("script_generate".equals(request.getSceneCode()) || "short_story_generate".equals(request.getSceneCode())) {
|
||||
Boolean useSocialInsights = request.getInputs().getBoolean("useSocialInsights");
|
||||
String socialInsightContext = scriptContextService.buildSocialInsightContext(userId, useSocialInsights);
|
||||
if (StringUtils.hasText(socialInsightContext)) {
|
||||
request.getInputs().put("socialInsightContext", socialInsightContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeErrorCode(Exception e) {
|
||||
String message = e.getMessage();
|
||||
if (message != null && message.startsWith("AI_")) {
|
||||
return message;
|
||||
}
|
||||
if (message != null && message.contains("timed out")) {
|
||||
return "AI_STREAM_TIMEOUT";
|
||||
}
|
||||
return "AI_STREAM_INTERRUPTED";
|
||||
}
|
||||
|
||||
private Map<String, Object> recoveredDoneMetadata(String requestId, int streamChunks, Long durationMs, String code, String message) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("requestId", requestId);
|
||||
metadata.put("streamChunks", streamChunks);
|
||||
metadata.put("durationMs", durationMs);
|
||||
metadata.put("recovered", true);
|
||||
metadata.put("warningCode", code);
|
||||
if (StringUtils.hasText(message)) {
|
||||
metadata.put("warningMessage", message);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private void emitDone(Consumer<AiStreamEvent> consumer, Map<String, Object> metadata) {
|
||||
try {
|
||||
consumer.accept(AiStreamEvent.done(metadata));
|
||||
} catch (Exception e) {
|
||||
log.debug("AI stream done event skipped after output persisted: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveOrUpdateLog(AiCallLog callLog) {
|
||||
if (StringUtils.hasText(callLog.getId())) {
|
||||
callLogService.updateById(callLog);
|
||||
} else {
|
||||
callLogService.save(callLog);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RuntimeTarget {
|
||||
private final AiSceneBinding scene;
|
||||
private final AiEndpointConfig endpoint;
|
||||
private final AiProvider provider;
|
||||
private final AiProviderAdapter adapter;
|
||||
|
||||
private RuntimeTarget(AiSceneBinding scene, AiEndpointConfig endpoint, AiProvider provider, AiProviderAdapter adapter) {
|
||||
this.scene = scene;
|
||||
this.endpoint = endpoint;
|
||||
this.provider = provider;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.entity.AiSceneBinding;
|
||||
import com.emotion.mapper.AiSceneBindingMapper;
|
||||
import com.emotion.service.AiSceneBindingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AiSceneBindingServiceImpl extends ServiceImpl<AiSceneBindingMapper, AiSceneBinding> implements AiSceneBindingService {
|
||||
|
||||
@Override
|
||||
public List<AiSceneBinding> listVisible() {
|
||||
return list(new LambdaQueryWrapper<AiSceneBinding>()
|
||||
.eq(AiSceneBinding::getIsDeleted, 0)
|
||||
.orderByDesc(AiSceneBinding::getPriority)
|
||||
.orderByDesc(AiSceneBinding::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiSceneBinding resolveScene(String sceneCode) {
|
||||
return getOne(new LambdaQueryWrapper<AiSceneBinding>()
|
||||
.eq(AiSceneBinding::getSceneCode, sceneCode)
|
||||
.eq(AiSceneBinding::getIsEnabled, 1)
|
||||
.eq(AiSceneBinding::getIsDeleted, 0)
|
||||
.orderByDesc(AiSceneBinding::getPriority)
|
||||
.last("limit 1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.dto.request.analytics.AnalyticsEventBatchRequest;
|
||||
import com.emotion.dto.request.analytics.AnalyticsEventRequest;
|
||||
import com.emotion.dto.request.analytics.AnalyticsQueryRequest;
|
||||
import com.emotion.dto.response.analytics.AnalyticsBatchResponse;
|
||||
import com.emotion.dto.response.analytics.AnalyticsFunnelItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsOverviewResponse;
|
||||
import com.emotion.dto.response.analytics.AnalyticsPreferenceItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTopEventItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTrendItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsUserItem;
|
||||
import com.emotion.entity.AnalyticsEvent;
|
||||
import com.emotion.mapper.AnalyticsEventMapper;
|
||||
import com.emotion.service.AnalyticsService;
|
||||
import com.emotion.service.analytics.AnalyticsDictionary;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class AnalyticsServiceImpl extends ServiceImpl<AnalyticsEventMapper, AnalyticsEvent>
|
||||
implements AnalyticsService {
|
||||
|
||||
private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]{1,99}$");
|
||||
private static final int JSON_LIMIT_BYTES = 4096;
|
||||
private static final int DEFAULT_LIMIT = 20;
|
||||
private static final int MAX_LIMIT = 100;
|
||||
private static final List<String> BLOCKED_PROPERTY_KEYS = List.of(
|
||||
"token", "access_token", "refresh_token", "password", "phone", "smsCode", "content", "fullContent"
|
||||
);
|
||||
private static final List<String> FUNNEL_EVENTS = List.of(
|
||||
"app_launch", "page_view", "script_inspiration_click", "script_generate_start",
|
||||
"script_generate_success", "script_detail_view", "path_select", "script_tts_play"
|
||||
);
|
||||
private static final List<String> PREFERENCE_DIMENSIONS = List.of("style", "length", "source", "tab", "platform");
|
||||
|
||||
public static boolean isSafeEventName(String eventName) {
|
||||
return StringUtils.hasText(eventName) && SAFE_NAME.matcher(eventName).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnalyticsBatchResponse ingestBatch(AnalyticsEventBatchRequest request) {
|
||||
if (request == null || CollectionUtils.isEmpty(request.getEvents())) {
|
||||
return AnalyticsBatchResponse.builder().accepted(0).rejected(0).build();
|
||||
}
|
||||
|
||||
List<AnalyticsEvent> events = new ArrayList<>();
|
||||
int rejected = 0;
|
||||
LocalDateTime serverTime = LocalDateTime.now();
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
Map<String, Object> deviceInfo = sanitizeJsonMap(request.getDeviceInfo());
|
||||
|
||||
for (AnalyticsEventRequest eventRequest : request.getEvents()) {
|
||||
if (!isValidEvent(eventRequest)) {
|
||||
rejected++;
|
||||
continue;
|
||||
}
|
||||
|
||||
AnalyticsEvent event = new AnalyticsEvent();
|
||||
event.setUserId(currentUserId);
|
||||
event.setAnonymousId(trimToNull(request.getAnonymousId()));
|
||||
event.setSessionId(request.getSessionId());
|
||||
event.setEventName(eventRequest.getEventName());
|
||||
event.setEventType(eventRequest.getEventType());
|
||||
event.setPagePath(trimToNull(eventRequest.getPagePath()));
|
||||
event.setReferrerPath(trimToNull(eventRequest.getReferrerPath()));
|
||||
event.setProperties(sanitizeJsonMap(eventRequest.getProperties()));
|
||||
event.setDeviceInfo(deviceInfo);
|
||||
event.setDurationMs(sanitizeDuration(eventRequest.getDurationMs()));
|
||||
event.setOccurredAt(sanitizeOccurredAt(eventRequest.getOccurredAt(), serverTime));
|
||||
event.setServerTime(serverTime);
|
||||
events.add(event);
|
||||
}
|
||||
|
||||
if (!events.isEmpty()) {
|
||||
saveBatch(events);
|
||||
}
|
||||
|
||||
return AnalyticsBatchResponse.builder()
|
||||
.accepted(events.size())
|
||||
.rejected(rejected)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnalyticsOverviewResponse getOverview(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
return AnalyticsOverviewResponse.builder()
|
||||
.pv(defaultLong(baseMapper.countEvent("page_view", range.start, range.end)))
|
||||
.uv(defaultLong(baseMapper.countUniqueVisitors(range.start, range.end)))
|
||||
.eventCount(defaultLong(baseMapper.countAll(range.start, range.end)))
|
||||
.activeUsers(defaultLong(baseMapper.countActiveUsers(range.start, range.end)))
|
||||
.ttsRequests(defaultLong(baseMapper.countEvent("script_tts_request", range.start, range.end)))
|
||||
.ttsPlays(defaultLong(baseMapper.countEvent("script_tts_play", range.start, range.end)))
|
||||
.avgStayMs(defaultDouble(baseMapper.avgStayMs(range.start, range.end)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnalyticsTrendItem> getTrend(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
List<AnalyticsTrendItem> result = baseMapper.selectTrend(range.start, range.end, resolveDateFormat(request));
|
||||
result.forEach(this::fillTrendLabels);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnalyticsFunnelItem> getFunnel(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
Map<String, Long> usersByEvent = new HashMap<>();
|
||||
for (AnalyticsTopEventItem item : baseMapper.selectFunnelUsers(range.start, range.end)) {
|
||||
usersByEvent.put(item.getEventName(), item.getUsers());
|
||||
}
|
||||
|
||||
List<AnalyticsFunnelItem> result = new ArrayList<>();
|
||||
long firstUsers = 0L;
|
||||
for (String eventName : FUNNEL_EVENTS) {
|
||||
long users = usersByEvent.getOrDefault(eventName, 0L);
|
||||
if (firstUsers == 0L && users > 0L) {
|
||||
firstUsers = users;
|
||||
}
|
||||
double conversionRate = firstUsers == 0L ? 0D : (double) users / firstUsers;
|
||||
result.add(AnalyticsFunnelItem.builder()
|
||||
.eventName(eventName)
|
||||
.label(AnalyticsDictionary.eventLabel(eventName))
|
||||
.users(users)
|
||||
.conversionRate(conversionRate)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnalyticsPreferenceItem> getPreferences(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
int perDimensionLimit = Math.max(3, Math.min(10, resolveLimit(request)));
|
||||
List<AnalyticsPreferenceItem> result = new ArrayList<>();
|
||||
for (String dimension : PREFERENCE_DIMENSIONS) {
|
||||
result.addAll(baseMapper.selectPreference(dimension, range.start, range.end, perDimensionLimit));
|
||||
}
|
||||
result.forEach(this::fillPreferenceLabels);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnalyticsTopEventItem> getTopEvents(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
List<AnalyticsTopEventItem> result = baseMapper.selectTopEvents(range.start, range.end, resolveLimit(request));
|
||||
result.forEach(this::fillTopEventLabels);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnalyticsUserItem> getUsers(AnalyticsQueryRequest request) {
|
||||
DateRange range = resolveRange(request);
|
||||
return baseMapper.selectUsers(range.start, range.end, resolveLimit(request));
|
||||
}
|
||||
|
||||
private boolean isValidEvent(AnalyticsEventRequest request) {
|
||||
return request != null
|
||||
&& isSafeEventName(request.getEventName())
|
||||
&& isSafeEventName(request.getEventType())
|
||||
&& fitsJsonLimit(request.getProperties());
|
||||
}
|
||||
|
||||
private Map<String, Object> sanitizeJsonMap(Map<String, Object> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> sanitized = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (!StringUtils.hasText(key) || BLOCKED_PROPERTY_KEYS.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
sanitized.put(key, entry.getValue());
|
||||
}
|
||||
if (sanitized.isEmpty() || !fitsJsonLimit(sanitized)) {
|
||||
return null;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private boolean fitsJsonLimit(Map<String, Object> value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return JSON.toJSONString(value).getBytes(StandardCharsets.UTF_8).length <= JSON_LIMIT_BYTES;
|
||||
}
|
||||
|
||||
private Long sanitizeDuration(Long durationMs) {
|
||||
if (durationMs == null || durationMs < 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.min(durationMs, 24L * 60L * 60L * 1000L);
|
||||
}
|
||||
|
||||
private LocalDateTime sanitizeOccurredAt(LocalDateTime occurredAt, LocalDateTime fallback) {
|
||||
if (occurredAt == null) {
|
||||
return fallback;
|
||||
}
|
||||
LocalDateTime earliest = fallback.minusDays(30);
|
||||
LocalDateTime latest = fallback.plusMinutes(10);
|
||||
if (occurredAt.isBefore(earliest) || occurredAt.isAfter(latest)) {
|
||||
return fallback;
|
||||
}
|
||||
return occurredAt;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private DateRange resolveRange(AnalyticsQueryRequest request) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate endDate = request != null && request.getEndDate() != null ? request.getEndDate() : today;
|
||||
LocalDate startDate = request != null && request.getStartDate() != null ? request.getStartDate() : endDate.minusDays(6);
|
||||
if (startDate.isAfter(endDate)) {
|
||||
startDate = endDate;
|
||||
}
|
||||
if (startDate.isBefore(endDate.minusDays(89))) {
|
||||
startDate = endDate.minusDays(89);
|
||||
}
|
||||
return new DateRange(startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay());
|
||||
}
|
||||
|
||||
private String resolveDateFormat(AnalyticsQueryRequest request) {
|
||||
if (request != null && "hour".equalsIgnoreCase(request.getGranularity())) {
|
||||
return "%Y-%m-%d %H:00";
|
||||
}
|
||||
if (request != null && "month".equalsIgnoreCase(request.getGranularity())) {
|
||||
return "%Y-%m";
|
||||
}
|
||||
return "%Y-%m-%d";
|
||||
}
|
||||
|
||||
private int resolveLimit(AnalyticsQueryRequest request) {
|
||||
int limit = request != null && request.getLimit() != null ? request.getLimit() : DEFAULT_LIMIT;
|
||||
return Math.max(1, Math.min(MAX_LIMIT, limit));
|
||||
}
|
||||
|
||||
private void fillTopEventLabels(AnalyticsTopEventItem item) {
|
||||
item.setEventLabel(AnalyticsDictionary.eventLabel(item.getEventName()));
|
||||
item.setEventTypeLabel(AnalyticsDictionary.eventTypeLabel(item.getEventType()));
|
||||
item.setPageLabel(AnalyticsDictionary.pageLabel(item.getPagePath()));
|
||||
item.setApiLabel(AnalyticsDictionary.apiLabel(item.getApiPath()));
|
||||
}
|
||||
|
||||
private void fillTrendLabels(AnalyticsTrendItem item) {
|
||||
item.setEventLabel(AnalyticsDictionary.eventLabel(item.getEventName()));
|
||||
}
|
||||
|
||||
private void fillPreferenceLabels(AnalyticsPreferenceItem item) {
|
||||
item.setDimensionLabel(AnalyticsDictionary.dimensionLabel(item.getDimension()));
|
||||
item.setValueLabel(AnalyticsDictionary.valueLabel(item.getDimension(), item.getValue()));
|
||||
}
|
||||
|
||||
private long defaultLong(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
private double defaultDouble(Double value) {
|
||||
return value == null ? 0D : value;
|
||||
}
|
||||
|
||||
private static class DateRange {
|
||||
private final LocalDateTime start;
|
||||
private final LocalDateTime end;
|
||||
|
||||
private DateRange(LocalDateTime start, LocalDateTime end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.emotion.dto.request.ApiEndpointListRequest;
|
||||
import com.emotion.dto.response.ApiEndpointDetailResponse;
|
||||
import com.emotion.dto.response.ApiEndpointItemResponse;
|
||||
import com.emotion.dto.response.ApiParamItemResponse;
|
||||
import com.emotion.entity.ApiEndpoint;
|
||||
import com.emotion.entity.ApiParam;
|
||||
import com.emotion.mapper.ApiEndpointMapper;
|
||||
import com.emotion.mapper.ApiParamMapper;
|
||||
import com.emotion.service.ApiEndpointService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 接口端点服务实现
|
||||
*
|
||||
* @author Peanut
|
||||
* @date 2026-05-23
|
||||
*/
|
||||
@Service
|
||||
public class ApiEndpointServiceImpl implements ApiEndpointService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiEndpointServiceImpl.class);
|
||||
private static final int MAX_REF_DEPTH = 10;
|
||||
|
||||
@Autowired
|
||||
private ApiEndpointMapper endpointMapper;
|
||||
|
||||
@Autowired
|
||||
private ApiParamMapper paramMapper;
|
||||
|
||||
@Value("${server.port:19089}")
|
||||
private int serverPort;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public IPage<ApiEndpointItemResponse> getPage(ApiEndpointListRequest request) {
|
||||
Page<ApiEndpoint> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<ApiEndpoint> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(ApiEndpoint::getPath, request.getKeyword())
|
||||
.or().like(ApiEndpoint::getSummary, request.getKeyword())
|
||||
.or().like(ApiEndpoint::getOperationId, request.getKeyword()));
|
||||
}
|
||||
if (StringUtils.hasText(request.getMethod())) {
|
||||
wrapper.eq(ApiEndpoint::getMethod, request.getMethod());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTags())) {
|
||||
wrapper.like(ApiEndpoint::getTags, request.getTags());
|
||||
}
|
||||
if (request.getDeprecated() != null) {
|
||||
wrapper.eq(ApiEndpoint::getDeprecated, request.getDeprecated());
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(ApiEndpoint::getCreateTime);
|
||||
|
||||
IPage<ApiEndpoint> result = endpointMapper.selectPage(page, wrapper);
|
||||
return result.convert(this::toItemResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiEndpointDetailResponse getDetail(String operationId) {
|
||||
LambdaQueryWrapper<ApiEndpoint> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(ApiEndpoint::getOperationId, operationId);
|
||||
ApiEndpoint endpoint = endpointMapper.selectOne(wrapper);
|
||||
if (endpoint == null) return null;
|
||||
|
||||
ApiEndpointDetailResponse response = toDetailResponse(endpoint);
|
||||
|
||||
LambdaQueryWrapper<ApiParam> paramWrapper = new LambdaQueryWrapper<>();
|
||||
paramWrapper.eq(ApiParam::getEndpointId, endpoint.getId());
|
||||
paramWrapper.orderByAsc(ApiParam::getName);
|
||||
List<ApiParam> params = paramMapper.selectList(paramWrapper);
|
||||
response.setParams(params.stream().map(this::toParamItemResponse).toList());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void syncFromOpenApi() {
|
||||
String url = "http://127.0.0.1:" + serverPort + "/api/v3/api-docs";
|
||||
log.info("开始同步接口数据,URL: {}", url);
|
||||
|
||||
String json;
|
||||
try {
|
||||
json = restTemplate.getForObject(url, String.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 OpenAPI spec 失败: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (json == null || json.isBlank()) {
|
||||
log.warn("OpenAPI spec 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
JsonNode components = root.path("components");
|
||||
JsonNode schemas = components.path("schemas");
|
||||
|
||||
paramMapper.delete(null);
|
||||
endpointMapper.delete(null);
|
||||
log.info("已清空旧接口数据");
|
||||
|
||||
JsonNode paths = root.path("paths");
|
||||
Iterator<Map.Entry<String, JsonNode>> pathEntries = paths.fields();
|
||||
int count = 0;
|
||||
java.util.Set<String> seenOperationIds = new java.util.HashSet<>();
|
||||
|
||||
while (pathEntries.hasNext()) {
|
||||
Map.Entry<String, JsonNode> pathEntry = pathEntries.next();
|
||||
String path = pathEntry.getKey();
|
||||
JsonNode methods = pathEntry.getValue();
|
||||
|
||||
for (Iterator<String> it = methods.fieldNames(); it.hasNext(); ) {
|
||||
String methodKey = it.next();
|
||||
String method = methodKey.toUpperCase();
|
||||
// Skip non-HTTP-method keys (e.g., summary, description at path level)
|
||||
if (!isHttpMethod(method)) continue;
|
||||
|
||||
JsonNode endpointNode = methods.get(methodKey);
|
||||
|
||||
String operationId = endpointNode.path("operationId").asText();
|
||||
if (operationId.isEmpty()) continue;
|
||||
if (seenOperationIds.contains(operationId)) {
|
||||
log.warn("跳过重复 operationId: {} ({} {})", operationId, path, method);
|
||||
continue;
|
||||
}
|
||||
seenOperationIds.add(operationId);
|
||||
|
||||
ApiEndpoint apiEndpoint = ApiEndpoint.builder()
|
||||
.path(path)
|
||||
.method(method)
|
||||
.operationId(operationId)
|
||||
.summary(endpointNode.path("summary").asText(null))
|
||||
.description(endpointNode.path("description").asText(null))
|
||||
.deprecated(endpointNode.path("deprecated").asBoolean(false) ? 1 : 0)
|
||||
.build();
|
||||
|
||||
JsonNode tagsNode = endpointNode.path("tags");
|
||||
if (tagsNode.isArray() && tagsNode.size() > 0) {
|
||||
List<String> tagList = new ArrayList<>();
|
||||
for (JsonNode t : tagsNode) tagList.add(t.asText());
|
||||
apiEndpoint.setTags(String.join(",", tagList));
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
List<ApiParam> paramList = new ArrayList<>();
|
||||
JsonNode parameters = endpointNode.path("parameters");
|
||||
if (parameters.isArray()) {
|
||||
for (JsonNode param : parameters) {
|
||||
ApiParam apiParam = parseParam(param, schemas, 0);
|
||||
if (apiParam != null) {
|
||||
paramList.add(apiParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse requestBody schema
|
||||
JsonNode requestBody = endpointNode.path("requestBody");
|
||||
if (!requestBody.isMissingNode()) {
|
||||
JsonNode content = requestBody.path("content");
|
||||
if (!content.isMissingNode()) {
|
||||
apiEndpoint.setRequestSchema(resolveSchema(content, schemas, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse responses schema
|
||||
JsonNode responses = endpointNode.path("responses");
|
||||
if (!responses.isMissingNode()) {
|
||||
apiEndpoint.setResponseSchema(responses.toString());
|
||||
}
|
||||
|
||||
endpointMapper.insert(apiEndpoint);
|
||||
|
||||
for (ApiParam p : paramList) {
|
||||
p.setEndpointId(apiEndpoint.getId());
|
||||
paramMapper.insert(p);
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("同步完成,共同步 {} 个接口", count);
|
||||
} catch (Exception e) {
|
||||
log.error("同步接口数据失败", e);
|
||||
throw new RuntimeException("同步接口数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHttpMethod(String method) {
|
||||
return method.equals("GET") || method.equals("POST") || method.equals("PUT")
|
||||
|| method.equals("DELETE") || method.equals("PATCH")
|
||||
|| method.equals("HEAD") || method.equals("OPTIONS");
|
||||
}
|
||||
|
||||
private ApiParam parseParam(JsonNode paramNode, JsonNode schemas, int depth) {
|
||||
if (depth > MAX_REF_DEPTH) return null;
|
||||
|
||||
String name = paramNode.path("name").asText(null);
|
||||
if (name == null) return null;
|
||||
|
||||
JsonNode schemaNode = paramNode.path("schema");
|
||||
String typeDef = schemaNode.path("type").asText(null);
|
||||
|
||||
// Parse enum values if present
|
||||
String enumValues = null;
|
||||
JsonNode enumArray = schemaNode.path("enum");
|
||||
if (enumArray.isArray() && enumArray.size() > 0) {
|
||||
List<String> values = new ArrayList<>();
|
||||
for (JsonNode v : enumArray) values.add(v.asText());
|
||||
enumValues = String.join(",", values);
|
||||
}
|
||||
|
||||
String defaultValue = schemaNode.path("default").asText(null);
|
||||
|
||||
return ApiParam.builder()
|
||||
.paramType(paramNode.path("in").asText(null))
|
||||
.name(name)
|
||||
.required(paramNode.path("required").asBoolean(false) ? 1 : 0)
|
||||
.paramTypeDef(typeDef)
|
||||
.description(paramNode.path("description").asText(null))
|
||||
.defaultValue(defaultValue)
|
||||
.enumValues(enumValues)
|
||||
.example(paramNode.path("example").asText(null))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String resolveSchema(JsonNode content, JsonNode schemas, int depth) {
|
||||
if (depth > MAX_REF_DEPTH) return "{}";
|
||||
|
||||
JsonNode appJson = content.path("application/json");
|
||||
if (appJson.isMissingNode()) return "{}";
|
||||
|
||||
JsonNode schema = appJson.path("schema");
|
||||
return expandRef(schema, schemas, depth);
|
||||
}
|
||||
|
||||
private String expandRef(JsonNode node, JsonNode schemas, int depth) {
|
||||
if (depth > MAX_REF_DEPTH) return "{\"$ref\": \"max depth exceeded\"}";
|
||||
|
||||
if (node.has("$ref")) {
|
||||
String ref = node.path("$ref").asText();
|
||||
if (ref.startsWith("#/components/schemas/")) {
|
||||
String schemaName = ref.substring("#/components/schemas/".length());
|
||||
JsonNode schemaNode = schemas.path(schemaName);
|
||||
if (!schemaNode.isMissingNode()) {
|
||||
return expandRef(schemaNode, schemas, depth + 1);
|
||||
}
|
||||
}
|
||||
return "{\"$ref\": \"" + ref + "\"}";
|
||||
}
|
||||
|
||||
if (node.has("properties")) {
|
||||
JsonNode props = node.path("properties");
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Iterator<Map.Entry<String, JsonNode>> it = props.fields(); it.hasNext(); ) {
|
||||
Map.Entry<String, JsonNode> entry = it.next();
|
||||
if (!first) sb.append(",");
|
||||
first = false;
|
||||
sb.append("\"").append(entry.getKey()).append("\":")
|
||||
.append(expandRef(entry.getValue(), schemas, depth + 1));
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
return node.toString();
|
||||
}
|
||||
|
||||
private ApiEndpointItemResponse toItemResponse(ApiEndpoint e) {
|
||||
ApiEndpointItemResponse r = new ApiEndpointItemResponse();
|
||||
r.setId(e.getId());
|
||||
r.setPath(e.getPath());
|
||||
r.setMethod(e.getMethod());
|
||||
r.setOperationId(e.getOperationId());
|
||||
r.setSummary(e.getSummary());
|
||||
r.setTags(e.getTags());
|
||||
r.setDeprecated(e.getDeprecated());
|
||||
r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null);
|
||||
return r;
|
||||
}
|
||||
|
||||
private ApiEndpointDetailResponse toDetailResponse(ApiEndpoint e) {
|
||||
ApiEndpointDetailResponse r = new ApiEndpointDetailResponse();
|
||||
r.setId(e.getId());
|
||||
r.setPath(e.getPath());
|
||||
r.setMethod(e.getMethod());
|
||||
r.setOperationId(e.getOperationId());
|
||||
r.setSummary(e.getSummary());
|
||||
r.setDescription(e.getDescription());
|
||||
r.setTags(e.getTags());
|
||||
r.setDeprecated(e.getDeprecated());
|
||||
r.setRequestSchema(e.getRequestSchema());
|
||||
r.setResponseSchema(e.getResponseSchema());
|
||||
r.setCreateTime(e.getCreateTime() != null ? e.getCreateTime().toString() : null);
|
||||
return r;
|
||||
}
|
||||
|
||||
private ApiParamItemResponse toParamItemResponse(ApiParam p) {
|
||||
ApiParamItemResponse r = new ApiParamItemResponse();
|
||||
r.setParamType(p.getParamType());
|
||||
r.setName(p.getName());
|
||||
r.setRequired(p.getRequired());
|
||||
r.setParamTypeDef(p.getParamTypeDef());
|
||||
r.setDescription(p.getDescription());
|
||||
r.setDefaultValue(p.getDefaultValue());
|
||||
r.setEnumValues(p.getEnumValues());
|
||||
r.setExample(p.getExample());
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.dto.response.asr.AsrTranscribeResponse;
|
||||
import com.emotion.service.AsrService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class AsrServiceImpl implements AsrService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${emotion.asr.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${emotion.asr.engine-url:http://127.0.0.1:19120}")
|
||||
private String engineUrl;
|
||||
|
||||
@Value("${emotion.asr.max-file-size:10485760}")
|
||||
private long maxFileSize;
|
||||
|
||||
@Value("${emotion.asr.allowed-types:wav,mp3,m4a,mp4,aac,amr}")
|
||||
private String allowedTypes;
|
||||
|
||||
public AsrServiceImpl(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsrTranscribeResponse transcribe(MultipartFile file) {
|
||||
validate(file);
|
||||
try {
|
||||
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : "voice.wav";
|
||||
}
|
||||
};
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", resource);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(engineUrl + "/transcribe", request, Map.class);
|
||||
Map<?, ?> data = response.getBody();
|
||||
boolean success = data != null && Boolean.TRUE.equals(data.get("success"));
|
||||
if (!success) {
|
||||
String message = data == null ? "ASR service returned empty response" : String.valueOf(data.get("errorMessage"));
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
return AsrTranscribeResponse.builder()
|
||||
.text(stringValue(data.get("text")))
|
||||
.language(stringValue(data.get("language")))
|
||||
.durationMs(longValue(data.get("durationMs")))
|
||||
.engine(stringValue(data.get("engine")))
|
||||
.model(stringValue(data.get("model")))
|
||||
.build();
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("语音识别服务暂时不可用: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(MultipartFile file) {
|
||||
if (!enabled) {
|
||||
throw new IllegalStateException("语音识别功能未启用");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请上传语音文件");
|
||||
}
|
||||
if (file.getSize() > maxFileSize) {
|
||||
throw new IllegalArgumentException("语音文件过大,请控制在10MB以内");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
String extension = "";
|
||||
if (StringUtils.hasText(filename) && filename.contains(".")) {
|
||||
extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
Set<String> allowed = Set.of(allowedTypes.toLowerCase(Locale.ROOT).split(","));
|
||||
if (StringUtils.hasText(extension) && !allowed.contains(extension)) {
|
||||
throw new IllegalArgumentException("不支持的语音格式: " + extension);
|
||||
}
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
if (value == null) return null;
|
||||
String text = String.valueOf(value);
|
||||
return "null".equals(text) ? null : text;
|
||||
}
|
||||
|
||||
private Long longValue(Object value) {
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
import com.emotion.dto.response.CaptchaResponse;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
import com.emotion.dto.response.SmsCodeResponse;
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
import com.emotion.entity.User;
|
||||
import com.emotion.exception.AuthException;
|
||||
import com.emotion.exception.BusinessException;
|
||||
import com.emotion.exception.CaptchaException;
|
||||
import com.emotion.exception.TokenException;
|
||||
import com.emotion.service.AuthService;
|
||||
import com.emotion.service.UserService;
|
||||
import com.emotion.service.WechatMiniProgramService;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import com.emotion.util.TokenUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private WechatMiniProgramService wechatMiniProgramService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private TokenUtil tokenUtil;
|
||||
|
||||
private static final String CAPTCHA_PREFIX = "captcha:";
|
||||
private static final String SMS_CODE_PREFIX = "sms_code:";
|
||||
private static final String TOKEN_PREFIX = "token:";
|
||||
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
|
||||
private static final int CAPTCHA_EXPIRE_MINUTES = 5;
|
||||
private static final int SMS_CODE_EXPIRE_MINUTES = 5;
|
||||
private static final int TOKEN_EXPIRE_HOURS = 24;
|
||||
private static final int REFRESH_TOKEN_EXPIRE_DAYS = 7;
|
||||
private static final String DEFAULT_SMS_CODE = "123456";
|
||||
private static final String WECHAT_MP_TYPE = "wechat-mp";
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public AuthResponse login(LoginRequest request) {
|
||||
// 验证短信验证码
|
||||
if (!validateSmsCode(request.getPhone(), request.getSmsCode())) {
|
||||
throw new CaptchaException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 根据手机号查询用户
|
||||
User user = userService.getByPhone(request.getPhone());
|
||||
|
||||
// 如果用户不存在,则自动注册
|
||||
if (user == null) {
|
||||
log.info("用户不存在,自动注册: phone={}", request.getPhone());
|
||||
user = autoRegisterUser(request.getPhone());
|
||||
} else {
|
||||
// 检查用户状态
|
||||
if (user.getStatus() != 1) {
|
||||
throw new AuthException("账号已被禁用");
|
||||
}
|
||||
log.info("用户登录: phone={}, userId={}", request.getPhone(), user.getId());
|
||||
}
|
||||
|
||||
// 生成令牌
|
||||
String accessToken = generateAccessToken(user);
|
||||
String refreshToken = generateRefreshToken(user);
|
||||
|
||||
// 更新用户最后活跃时间
|
||||
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
|
||||
|
||||
// 构建响应
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResponse wechatLogin(WechatLoginRequest request) {
|
||||
WechatCodeSessionResponse session = wechatMiniProgramService.code2Session(request.getCode());
|
||||
String openid = session.getOpenid();
|
||||
|
||||
User user = userService.getByThirdParty(WECHAT_MP_TYPE, openid);
|
||||
if (user == null) {
|
||||
user = createWechatUser(openid, request);
|
||||
log.info("Wechat mini program user created: userId={}", user.getId());
|
||||
} else if (user.getStatus() != 1) {
|
||||
throw new AuthException("账号已被禁用");
|
||||
}
|
||||
|
||||
String accessToken = generateAccessToken(user);
|
||||
String refreshToken = generateRefreshToken(user);
|
||||
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
|
||||
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResponse register(RegisterRequest request) {
|
||||
// 验证短信验证码
|
||||
if (!validateSmsCode(request.getPhone(), request.getSmsCode())) {
|
||||
throw new CaptchaException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (userService.getByPhone(request.getPhone()) != null) {
|
||||
throw new BusinessException("手机号已被注册");
|
||||
}
|
||||
|
||||
// 生成随机用户名:开心 + 6位随机大小写字母和数字
|
||||
String username = generateRandomUsername();
|
||||
|
||||
// 使用手机号作为账号
|
||||
String account = request.getPhone();
|
||||
|
||||
// 创建用户(密码在UserService中加密,这里不需要预先加密)
|
||||
User user = userService.createUser(
|
||||
account,
|
||||
username,
|
||||
request.getPassword(),
|
||||
null, // 邮箱为空
|
||||
request.getPhone()
|
||||
);
|
||||
|
||||
// 生成令牌
|
||||
String accessToken = generateAccessToken(user);
|
||||
String refreshToken = generateRefreshToken(user);
|
||||
|
||||
// 构建响应
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
|
||||
log.info("用户注册成功: phone={}, username={}", request.getPhone(), username);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResetPasswordResponse resetPassword(ResetPasswordRequest request) {
|
||||
// 验证码本期固定为123456
|
||||
if (request.getCaptcha() == null || !"123456".equals(request.getCaptcha().trim())) {
|
||||
throw new CaptchaException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 根据手机号查询用户
|
||||
User user = userService.getByPhone(request.getPhone());
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 使用统一的 PasswordEncoder 进行加密,保持与登录/注册一致
|
||||
String encoded = passwordEncoder.encode(request.getNewPassword());
|
||||
user.setPassword(encoded);
|
||||
userService.updateById(user);
|
||||
|
||||
ResetPasswordResponse resp = new ResetPasswordResponse();
|
||||
resp.setSuccess(true);
|
||||
resp.setMessage("重置密码成功");
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfoResponse getCurrentUserInfo(String userId) {
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw new AuthException("用户不存在");
|
||||
}
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaResponse generateCaptcha() {
|
||||
String captchaKey = UUID.randomUUID().toString();
|
||||
String captchaCode = generateCaptchaCode();
|
||||
|
||||
// 生成验证码图片
|
||||
String captchaImage = generateCaptchaImage(captchaCode);
|
||||
|
||||
// 存储验证码到Redis
|
||||
redisTemplate.opsForValue().set(
|
||||
CAPTCHA_PREFIX + captchaKey,
|
||||
captchaCode.toLowerCase(),
|
||||
CAPTCHA_EXPIRE_MINUTES,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
|
||||
CaptchaResponse response = new CaptchaResponse();
|
||||
response.setCaptchaKey(captchaKey);
|
||||
response.setCaptchaImage(captchaImage);
|
||||
response.setExpiresIn((long) CAPTCHA_EXPIRE_MINUTES * 60);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateCaptcha(String captchaKey, String captcha) {
|
||||
if (!StringUtils.hasText(captchaKey) || !StringUtils.hasText(captcha)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String storedCaptcha = (String) redisTemplate.opsForValue().get(CAPTCHA_PREFIX + captchaKey);
|
||||
if (storedCaptcha == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(CAPTCHA_PREFIX + captchaKey);
|
||||
|
||||
return storedCaptcha.equalsIgnoreCase(captcha.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout(String userId, String token) {
|
||||
// 删除访问令牌
|
||||
redisTemplate.delete(TOKEN_PREFIX + token);
|
||||
|
||||
// 删除刷新令牌(如果存在)
|
||||
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
|
||||
redisTemplate.delete(refreshTokenKey);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logoutByToken(HttpServletRequest request) {
|
||||
String token = tokenUtil.extractToken(request);
|
||||
String userId = validateTokenAndGetUserId(token);
|
||||
return logout(userId, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌并获取用户ID
|
||||
*/
|
||||
private String validateTokenAndGetUserId(String token) {
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
throw new TokenException("未提供访问令牌");
|
||||
}
|
||||
|
||||
if (!validateToken(token)) {
|
||||
throw new TokenException("访问令牌无效或已过期");
|
||||
}
|
||||
|
||||
String userId = getUserIdFromToken(token);
|
||||
if (userId == null) {
|
||||
throw new TokenException("访问令牌无效");
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResponse refreshToken(String refreshToken) {
|
||||
String userId = (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + refreshToken);
|
||||
if (userId == null) {
|
||||
throw new TokenException("刷新令牌无效或已过期");
|
||||
}
|
||||
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw new AuthException("用户不存在");
|
||||
}
|
||||
|
||||
// 生成新的访问令牌
|
||||
String newAccessToken = generateAccessToken(user);
|
||||
String newRefreshToken = generateRefreshToken(user);
|
||||
|
||||
// 删除旧的刷新令牌
|
||||
redisTemplate.delete(REFRESH_TOKEN_PREFIX + refreshToken);
|
||||
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken(newAccessToken);
|
||||
response.setRefreshToken(newRefreshToken);
|
||||
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
|
||||
response.setUserInfo(convertToUserInfoResponse(user));
|
||||
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(HttpServletRequest request) {
|
||||
String token = tokenUtil.extractToken(request);
|
||||
return validateToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 首先尝试JWT验证
|
||||
if (jwtUtil.validateToken(token)) {
|
||||
// JWT验证成功,再检查Redis中是否存在(用于登出功能)
|
||||
return redisTemplate.hasKey(TOKEN_PREFIX + token);
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warn("Token验证失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserIdFromToken(String token) {
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 首先尝试从JWT中获取用户ID
|
||||
String userId = jwtUtil.getUserIdFromToken(token);
|
||||
if (userId != null) {
|
||||
// 验证Redis中是否存在该token(确保未被登出)
|
||||
if (redisTemplate.hasKey(TOKEN_PREFIX + token)) {
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("从Token获取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsernameFromToken(String token) {
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接从JWT中获取用户名
|
||||
return jwtUtil.getUsernameFromToken(token);
|
||||
} catch (Exception e) {
|
||||
log.warn("从Token获取用户名失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌
|
||||
*/
|
||||
private String generateAccessToken(User user) {
|
||||
// 使用JWT生成token
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
|
||||
// 同时在Redis中存储token信息,用于快速验证和登出功能
|
||||
redisTemplate.opsForValue().set(
|
||||
TOKEN_PREFIX + token,
|
||||
user.getId(),
|
||||
TOKEN_EXPIRE_HOURS,
|
||||
TimeUnit.HOURS
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新令牌
|
||||
*/
|
||||
private String generateRefreshToken(User user) {
|
||||
// 使用JWT生成刷新token
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
|
||||
|
||||
// 在Redis中存储刷新token信息
|
||||
redisTemplate.opsForValue().set(
|
||||
REFRESH_TOKEN_PREFIX + refreshToken,
|
||||
user.getId(),
|
||||
REFRESH_TOKEN_EXPIRE_DAYS,
|
||||
TimeUnit.DAYS
|
||||
);
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
private String generateCaptchaCode() {
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
code.append(random.nextInt(10));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码图片
|
||||
*/
|
||||
private String generateCaptchaImage(String code) {
|
||||
int width = 120;
|
||||
int height = 40;
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
|
||||
// 设置背景色
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
// 设置字体
|
||||
g.setFont(new Font("Arial", Font.BOLD, 20));
|
||||
g.setColor(Color.BLACK);
|
||||
|
||||
// 绘制验证码
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);
|
||||
}
|
||||
|
||||
// 添加干扰线
|
||||
Random random = new Random();
|
||||
g.setColor(Color.GRAY);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int x1 = random.nextInt(width);
|
||||
int y1 = random.nextInt(height);
|
||||
int x2 = random.nextInt(width);
|
||||
int y2 = random.nextInt(height);
|
||||
g.drawLine(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
|
||||
// 转换为Base64
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);
|
||||
} catch (IOException e) {
|
||||
throw new CaptchaException("生成验证码图片失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
private boolean verifyPassword(String rawPassword, String encodedPassword) {
|
||||
// 使用BCrypt进行密码验证
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*/
|
||||
private String encryptPassword(String rawPassword) {
|
||||
// 使用BCrypt进行密码加密
|
||||
return passwordEncoder.encode(rawPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为用户信息响应
|
||||
*/
|
||||
private UserInfoResponse convertToUserInfoResponse(User user) {
|
||||
UserInfoResponse response = new UserInfoResponse();
|
||||
BeanUtils.copyProperties(user, response);
|
||||
if (user.getCreateTime() != null) {
|
||||
response.setCreateTime(user.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (user.getLastActiveTime() != null) {
|
||||
response.setLastActiveTime(user.getLastActiveTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByAccount(String account) {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
return false;
|
||||
}
|
||||
User user = userService.getByAccount(account);
|
||||
return user != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
if (!StringUtils.hasText(email)) {
|
||||
return false;
|
||||
}
|
||||
User user = userService.getByEmail(email);
|
||||
return user != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByPhone(String phone) {
|
||||
if (!StringUtils.hasText(phone)) {
|
||||
return false;
|
||||
}
|
||||
User user = userService.getByPhone(phone);
|
||||
return user != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsCodeResponse sendSmsCode(String phone) {
|
||||
// 验证手机号格式
|
||||
if (!StringUtils.hasText(phone) || !phone.matches("^1[3-9]\\d{9}$")) {
|
||||
throw new BusinessException("手机号格式不正确");
|
||||
}
|
||||
|
||||
// 检查手机号是否已注册,用于提示信息
|
||||
boolean isRegistered = existsByPhone(phone);
|
||||
String message;
|
||||
if (isRegistered) {
|
||||
message = "验证码已发送,用于登录验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟";
|
||||
} else {
|
||||
message = "验证码已发送,用于注册验证,有效期" + SMS_CODE_EXPIRE_MINUTES + "分钟";
|
||||
}
|
||||
|
||||
// TODO: 接入真实的短信服务商(阿里云、腾讯云等)
|
||||
// 目前使用固定验证码 123456
|
||||
String code = DEFAULT_SMS_CODE;
|
||||
|
||||
// 将验证码存储到Redis,有效期5分钟
|
||||
String key = SMS_CODE_PREFIX + phone;
|
||||
redisTemplate.opsForValue().set(key, code, SMS_CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||
|
||||
log.info("发送短信验证码: phone={}, code={}, isRegistered={}", phone, code, isRegistered);
|
||||
|
||||
// 构建响应
|
||||
return SmsCodeResponse.builder()
|
||||
.code(code) // 开发环境返回验证码,生产环境应该删除此行
|
||||
.expiresIn((long) SMS_CODE_EXPIRE_MINUTES * 60)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateSmsCode(String phone, String code) {
|
||||
if (!StringUtils.hasText(phone) || !StringUtils.hasText(code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String key = SMS_CODE_PREFIX + phone;
|
||||
String storedCode = (String) redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (storedCode == null) {
|
||||
log.warn("短信验证码不存在或已过期: phone={}", phone);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isValid = storedCode.equals(code);
|
||||
|
||||
// 验证成功后删除验证码(一次性使用)
|
||||
if (isValid) {
|
||||
redisTemplate.delete(key);
|
||||
log.info("短信验证码验证成功: phone={}", phone);
|
||||
} else {
|
||||
log.warn("短信验证码错误: phone={}, expected={}, actual={}", phone, storedCode, code);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机用户名
|
||||
* 格式:开心 + 6位随机大小写字母和数字
|
||||
*
|
||||
* @return 随机用户名
|
||||
*/
|
||||
private String generateRandomUsername() {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
Random random = new Random();
|
||||
StringBuilder sb = new StringBuilder("开心");
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int index = random.nextInt(chars.length());
|
||||
sb.append(chars.charAt(index));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动注册用户(用于登录时用户不存在的情况)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 新创建的用户
|
||||
*/
|
||||
private User autoRegisterUser(String phone) {
|
||||
// 生成随机用户名:开心 + 6位随机大小写字母和数字
|
||||
String username = generateRandomUsername();
|
||||
|
||||
// 使用手机号作为账号
|
||||
String account = phone;
|
||||
|
||||
// 生成随机密码(用户可以后续修改)
|
||||
String randomPassword = generateRandomPassword();
|
||||
|
||||
// 创建用户
|
||||
User user = userService.createUser(
|
||||
account,
|
||||
username,
|
||||
randomPassword,
|
||||
null, // 邮箱为空
|
||||
phone
|
||||
);
|
||||
|
||||
log.info("自动注册用户成功: phone={}, username={}, userId={}", phone, username, user.getId());
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机密码
|
||||
* 格式:8位随机大小写字母和数字
|
||||
*
|
||||
* @return 随机密码
|
||||
*/
|
||||
private User createWechatUser(String openid, WechatLoginRequest request) {
|
||||
String username = generateRandomUsername();
|
||||
User user = new User();
|
||||
user.setAccount("wx_" + openid);
|
||||
user.setUsername(username);
|
||||
user.setNickname(StringUtils.hasText(request.getNickname()) ? request.getNickname() : username);
|
||||
user.setAvatar(StringUtils.hasText(request.getAvatar()) ? request.getAvatar() : null);
|
||||
user.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||
user.setMemberLevel("free");
|
||||
user.setStatus(1);
|
||||
user.setIsVerified(1);
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
user.setThirdPartyType(WECHAT_MP_TYPE);
|
||||
user.setThirdPartyId(openid);
|
||||
userService.save(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private String generateRandomPassword() {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
Random random = new Random();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int index = random.nextInt(chars.length());
|
||||
sb.append(chars.charAt(index));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.comment.CommentCreateRequest;
|
||||
import com.emotion.dto.request.comment.CommentUpdateRequest;
|
||||
import com.emotion.dto.request.comment.CommentPageRequest;
|
||||
import com.emotion.dto.response.comment.CommentResponse;
|
||||
import com.emotion.entity.Comment;
|
||||
import com.emotion.mapper.CommentMapper;
|
||||
import com.emotion.service.CommentService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 评论服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<CommentResponse> getPage(CommentPageRequest request) {
|
||||
Page<Comment> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 根据请求参数构建查询条件
|
||||
if (StringUtils.hasText(request.getPostId())) {
|
||||
wrapper.eq(Comment::getPostId, request.getPostId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(Comment::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(Comment::getContent, request.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.eq(Comment::getIsDeleted, 0).orderByDesc(Comment::getCreateTime);
|
||||
page = this.page(page, wrapper);
|
||||
|
||||
return convertPageToPageResult(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentResponse getById(String id) {
|
||||
Comment comment = this.getBaseMapper().selectById(id);
|
||||
if (comment == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentResponse create(CommentCreateRequest request) {
|
||||
Comment comment = new Comment();
|
||||
comment.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
comment.setPostId(request.getPostId());
|
||||
|
||||
// 从上下文中获取当前用户ID
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId != null) {
|
||||
comment.setUserId(currentUserId);
|
||||
} else if (request.getUserId() != null) {
|
||||
// 如果上下文中没有用户ID,则使用请求中的用户ID(向后兼容)
|
||||
comment.setUserId(request.getUserId());
|
||||
} else {
|
||||
throw new IllegalArgumentException("用户ID不能为空");
|
||||
}
|
||||
|
||||
comment.setContent(request.getContent());
|
||||
comment.setReplyToId(request.getReplyToId());
|
||||
comment.setLikes(0);
|
||||
|
||||
this.save(comment);
|
||||
return convertToResponse(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentResponse update(CommentUpdateRequest request) {
|
||||
Comment comment = this.getBaseMapper().selectById(request.getId());
|
||||
if (comment == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getContent())) {
|
||||
comment.setContent(request.getContent());
|
||||
}
|
||||
|
||||
this.updateById(comment);
|
||||
return convertToResponse(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String id) {
|
||||
Comment comment = this.getBaseMapper().selectById(id);
|
||||
if (comment == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private CommentResponse convertToResponse(Comment comment) {
|
||||
CommentResponse response = new CommentResponse();
|
||||
BeanUtils.copyProperties(comment, response);
|
||||
response.setId(comment.getId());
|
||||
if (comment.getCreateTime() != null) {
|
||||
response.setCreateTime(comment.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (comment.getUpdateTime() != null) {
|
||||
response.setUpdateTime(comment.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页对象为PageResult对象
|
||||
*/
|
||||
private PageResult<CommentResponse> convertPageToPageResult(Page<Comment> page) {
|
||||
PageResult<CommentResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList()));
|
||||
return pageResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.community.CommunityPostCreateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostUpdateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostPageRequest;
|
||||
import com.emotion.dto.response.community.CommunityPostResponse;
|
||||
import com.emotion.entity.CommunityPost;
|
||||
import com.emotion.mapper.CommunityPostMapper;
|
||||
import com.emotion.service.CommunityPostService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 社区帖子服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class CommunityPostServiceImpl extends ServiceImpl<CommunityPostMapper, CommunityPost> implements CommunityPostService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<CommunityPostResponse> getPage(CommunityPostPageRequest request) {
|
||||
Page<CommunityPost> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<CommunityPost> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 根据请求参数构建查询条件
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(CommunityPost::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getType())) {
|
||||
wrapper.eq(CommunityPost::getType, request.getType());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getLocationId())) {
|
||||
wrapper.eq(CommunityPost::getLocationId, request.getLocationId());
|
||||
}
|
||||
|
||||
if (request.getPublicOnly() != null && request.getPublicOnly()) {
|
||||
wrapper.eq(CommunityPost::getIsPrivate, 0);
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(CommunityPost::getTitle, request.getKeyword())
|
||||
.or().like(CommunityPost::getContent, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(CommunityPost::getIsDeleted, 0).orderByDesc(CommunityPost::getCreateTime);
|
||||
page = this.page(page, wrapper);
|
||||
|
||||
return convertPageToResponse(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommunityPostResponse getById(String id) {
|
||||
CommunityPost post = this.getBaseMapper().selectById(id);
|
||||
if (post == null) {
|
||||
return null;
|
||||
}
|
||||
// 增加浏览数
|
||||
Integer viewCount = post.getViewCount() == null ? 1 : post.getViewCount() + 1;
|
||||
post.setViewCount(viewCount);
|
||||
this.updateById(post);
|
||||
|
||||
return convertToResponse(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommunityPostResponse create(CommunityPostCreateRequest request) {
|
||||
CommunityPost post = new CommunityPost();
|
||||
post.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
post.setUserId(request.getUserId());
|
||||
post.setTitle(request.getTitle());
|
||||
post.setContent(request.getContent());
|
||||
post.setType(request.getType());
|
||||
post.setLocationId(request.getLocationId());
|
||||
post.setTags(request.getTags());
|
||||
post.setIsPrivate(request.getIsPrivate() != null ? request.getIsPrivate() : 0); // 默认公开
|
||||
post.setLikes(0);
|
||||
post.setViewCount(0);
|
||||
post.setCommentCount(0);
|
||||
|
||||
this.save(post);
|
||||
return convertToResponse(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommunityPostResponse update(CommunityPostUpdateRequest request) {
|
||||
CommunityPost post = this.getBaseMapper().selectById(request.getId());
|
||||
if (post == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (request.getTitle() != null) {
|
||||
post.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getContent() != null) {
|
||||
post.setContent(request.getContent());
|
||||
}
|
||||
if (request.getType() != null) {
|
||||
post.setType(request.getType());
|
||||
}
|
||||
if (request.getLocationId() != null) {
|
||||
post.setLocationId(request.getLocationId());
|
||||
}
|
||||
if (request.getTags() != null) {
|
||||
post.setTags(request.getTags());
|
||||
}
|
||||
if (request.getIsPrivate() != null) {
|
||||
post.setIsPrivate(request.getIsPrivate());
|
||||
}
|
||||
|
||||
this.updateById(post);
|
||||
return convertToResponse(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String id) {
|
||||
CommunityPost post = this.getBaseMapper().selectById(id);
|
||||
if (post == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private CommunityPostResponse convertToResponse(CommunityPost post) {
|
||||
CommunityPostResponse response = new CommunityPostResponse();
|
||||
BeanUtils.copyProperties(post, response);
|
||||
response.setId(post.getId());
|
||||
if (post.getCreateTime() != null) {
|
||||
response.setCreateTime(post.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (post.getUpdateTime() != null) {
|
||||
response.setUpdateTime(post.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页对象为响应对象
|
||||
*/
|
||||
private PageResult<CommunityPostResponse> convertPageToResponse(Page<CommunityPost> page) {
|
||||
PageResult<CommunityPostResponse> responsePage = new PageResult<>();
|
||||
responsePage.setCurrent(page.getCurrent());
|
||||
responsePage.setSize(page.getSize());
|
||||
responsePage.setTotal(page.getTotal());
|
||||
responsePage.setPages(page.getPages());
|
||||
responsePage.setRecords(
|
||||
page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList()));
|
||||
return responsePage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.ConversationCreateRequest;
|
||||
import com.emotion.dto.request.ConversationPageRequest;
|
||||
import com.emotion.dto.response.ConversationResponse;
|
||||
import com.emotion.entity.Conversation;
|
||||
import com.emotion.mapper.ConversationMapper;
|
||||
import com.emotion.service.ConversationService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会话服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class ConversationServiceImpl extends ServiceImpl<ConversationMapper, Conversation> implements ConversationService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<Conversation> getPage(ConversationPageRequest request) {
|
||||
Page<Conversation> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 根据请求参数构建查询条件
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(Conversation::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
wrapper.eq(Conversation::getConversationStatus, request.getStatus());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getType())) {
|
||||
wrapper.eq(Conversation::getType, request.getType());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(Conversation::getTitle, request.getKeyword())
|
||||
.or().like(Conversation::getSummary, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(Conversation::getIsDeleted, 0).orderByDesc(Conversation::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Conversation> getPageByUserId(ConversationPageRequest request) {
|
||||
Page<Conversation> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 必须提供userId参数
|
||||
if (!StringUtils.hasText(request.getUserId())) {
|
||||
throw new IllegalArgumentException("userId不能为空");
|
||||
}
|
||||
|
||||
wrapper.eq(Conversation::getUserId, request.getUserId())
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Conversation> getByUserId(String userId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getUserId, userId)
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Conversation> getActiveByUserId(String userId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getUserId, userId)
|
||||
.eq(Conversation::getConversationStatus, "active")
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getLastActiveTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Conversation getByCozeConversationId(String cozeConversationId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getCozeConversationId, cozeConversationId)
|
||||
.eq(Conversation::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateMessageCount(String conversationId, Integer messageCount) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(conversationId);
|
||||
conversation.setMessageCount(messageCount);
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String conversationId, Integer status) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(conversationId);
|
||||
// 根据status值设置对应的状态字符串
|
||||
String statusStr = "active";
|
||||
if (status == 1) {
|
||||
statusStr = "ended";
|
||||
} else if (status == 2) {
|
||||
statusStr = "archived";
|
||||
}
|
||||
conversation.setConversationStatus(statusStr);
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateEndTime(String conversationId, LocalDateTime endTime) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(conversationId);
|
||||
conversation.setEndTime(endTime);
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserId(String userId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getUserId, userId)
|
||||
.eq(Conversation::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countActiveByUserId(String userId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getUserId, userId)
|
||||
.eq(Conversation::getConversationStatus, "active")
|
||||
.eq(Conversation::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Conversation> getForArchive(Integer days) {
|
||||
LocalDateTime archiveTime = LocalDateTime.now().minusDays(days);
|
||||
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getConversationStatus, "active")
|
||||
.lt(Conversation::getLastActiveTime, archiveTime)
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByAsc(Conversation::getLastActiveTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean batchArchive(List<String> conversationIds) {
|
||||
for (String conversationId : conversationIds) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(conversationId);
|
||||
conversation.setConversationStatus("archived");
|
||||
this.updateById(conversation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Conversation createConversation(String userId, String title, String cozeConversationId) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
conversation.setUserId(userId);
|
||||
conversation.setTitle(title);
|
||||
conversation.setCozeConversationId(cozeConversationId);
|
||||
conversation.setUserType("registered");
|
||||
conversation.setType("chat");
|
||||
conversation.setConversationStatus("active");
|
||||
conversation.setStartTime(LocalDateTime.now());
|
||||
conversation.setLastActiveTime(LocalDateTime.now());
|
||||
conversation.setMessageCount(0);
|
||||
conversation.setTotalTokens(0);
|
||||
conversation.setTotalCost(java.math.BigDecimal.ZERO);
|
||||
|
||||
this.save(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endConversation(String conversationId) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(conversationId);
|
||||
conversation.setConversationStatus("ended");
|
||||
conversation.setEndTime(LocalDateTime.now());
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ConversationResponse> getPageWithResponse(ConversationPageRequest request) {
|
||||
IPage<Conversation> page = this.getPage(request);
|
||||
return convertPageToResponse(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ConversationResponse> getPageByUserIdWithResponse(ConversationPageRequest request) {
|
||||
IPage<Conversation> page = this.getPageByUserId(request);
|
||||
return convertPageToResponse(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationResponse getConversationResponseById(String id) {
|
||||
Conversation conversation = this.getById(id);
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConversationResponse> getByUserIdWithResponse(String userId) {
|
||||
List<Conversation> conversations = this.getByUserId(userId);
|
||||
return conversations.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConversationResponse> getActiveConversationsWithResponse() {
|
||||
// 实现获取活跃对话的逻辑
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getConversationStatus, "active")
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getLastActiveTime);
|
||||
List<Conversation> conversations = this.list(wrapper);
|
||||
return conversations.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConversationResponse> getArchivedConversationsWithResponse() {
|
||||
// 实现获取归档对话的逻辑
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getConversationStatus, "archived")
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getCreateTime);
|
||||
List<Conversation> conversations = this.list(wrapper);
|
||||
return conversations.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean archiveConversation(String id) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(id);
|
||||
conversation.setConversationStatus("archived");
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean activateConversation(String id) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(id);
|
||||
conversation.setConversationStatus("active");
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationResponse createConversationWithResponse(ConversationCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
// 获取客户端IP地址
|
||||
String clientIp = UserContextUtils.getClientIpAddress(httpRequest);
|
||||
|
||||
Conversation conversation = this.createConversation(
|
||||
request.getUserId(),
|
||||
request.getTitle(),
|
||||
null // cozeConversationId
|
||||
);
|
||||
|
||||
// 设置客户端IP
|
||||
conversation.setClientIp(clientIp);
|
||||
this.updateById(conversation);
|
||||
|
||||
return convertToResponse(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationResponse updateConversationWithResponse(ConversationCreateRequest request) {
|
||||
Conversation conversation = this.getById(request.getId()); // 修复:使用ID而不是userId
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (request.getTitle() != null) {
|
||||
conversation.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getType() != null) {
|
||||
conversation.setType(request.getType());
|
||||
}
|
||||
|
||||
this.updateById(conversation);
|
||||
return convertToResponse(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateConversationStatus(String id, String status) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(id);
|
||||
conversation.setConversationStatus(status);
|
||||
return this.updateById(conversation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private ConversationResponse convertToResponse(Conversation conversation) {
|
||||
ConversationResponse response = new ConversationResponse();
|
||||
BeanUtils.copyProperties(conversation, response);
|
||||
response.setId(conversation.getId());
|
||||
response.setStatus(conversation.getConversationStatus());
|
||||
if (conversation.getCreateTime() != null) {
|
||||
response.setCreateTime(conversation.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (conversation.getUpdateTime() != null) {
|
||||
response.setUpdateTime(conversation.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (conversation.getLastActiveTime() != null) {
|
||||
response.setLastMessageTime(conversation.getLastActiveTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页对象为响应对象
|
||||
*/
|
||||
private PageResult<ConversationResponse> convertPageToResponse(IPage<Conversation> page) {
|
||||
PageResult<ConversationResponse> responsePage = new PageResult<>();
|
||||
responsePage.setCurrent(page.getCurrent());
|
||||
responsePage.setSize(page.getSize());
|
||||
responsePage.setTotal(page.getTotal());
|
||||
responsePage.setPages(page.getPages());
|
||||
responsePage.setRecords(
|
||||
page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList()));
|
||||
return responsePage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.coze.CozeApiCallPageRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallCreateRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallUpdateRequest;
|
||||
import com.emotion.dto.response.coze.CozeApiCallResponse;
|
||||
import com.emotion.entity.CozeApiCall;
|
||||
import com.emotion.mapper.CozeApiCallMapper;
|
||||
import com.emotion.service.CozeApiCallService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Coze API调用记录服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeApiCall> implements CozeApiCallService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<CozeApiCallResponse> getPage(CozeApiCallPageRequest request) {
|
||||
Page<CozeApiCall> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 根据请求参数构建查询条件
|
||||
if (StringUtils.hasText(request.getConversationId())) {
|
||||
wrapper.eq(CozeApiCall::getConversationId, request.getConversationId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(CozeApiCall::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getBotId())) {
|
||||
wrapper.eq(CozeApiCall::getBotId, request.getBotId());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
wrapper.eq(CozeApiCall::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getRequestType())) {
|
||||
wrapper.eq(CozeApiCall::getRequestType, request.getRequestType());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getTraceId())) {
|
||||
wrapper.eq(CozeApiCall::getTraceId, request.getTraceId());
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(CozeApiCall::getRequestUrl, request.getKeyword())
|
||||
.or().like(CozeApiCall::getAiReply, request.getKeyword())
|
||||
.or().like(CozeApiCall::getUserMessage, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(CozeApiCall::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(CozeApiCall::getStartTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(CozeApiCall::getStartTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(CozeApiCall::getStartTime);
|
||||
}
|
||||
|
||||
page = this.page(page, wrapper);
|
||||
return convertPageToPageResult(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCallResponse getById(String id) {
|
||||
CozeApiCall apiCall = this.getBaseMapper().selectById(id);
|
||||
if (apiCall == null || apiCall.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(apiCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCallResponse create(CozeApiCallCreateRequest request) {
|
||||
CozeApiCall apiCall = new CozeApiCall();
|
||||
apiCall.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
|
||||
// 复制属性
|
||||
apiCall.setConversationId(request.getConversationId());
|
||||
apiCall.setMessageId(request.getMessageId());
|
||||
apiCall.setCozeChatId(request.getCozeChatId());
|
||||
apiCall.setCozeConversationId(request.getCozeConversationId());
|
||||
apiCall.setBotId(request.getBotId());
|
||||
apiCall.setWorkflowId(request.getWorkflowId());
|
||||
apiCall.setUserId(request.getUserId());
|
||||
apiCall.setRequestType(request.getRequestType());
|
||||
apiCall.setRequestUrl(request.getRequestUrl());
|
||||
apiCall.setRequestBody(request.getRequestBody());
|
||||
apiCall.setRequestHeaders(request.getRequestHeaders());
|
||||
apiCall.setUserMessage(request.getUserMessage());
|
||||
apiCall.setUserMessageType(request.getUserMessageType());
|
||||
apiCall.setAiReply(request.getAiReply());
|
||||
apiCall.setAiReplyType(request.getAiReplyType());
|
||||
apiCall.setResponseStatus(request.getResponseStatus());
|
||||
apiCall.setResponseBody(request.getResponseBody());
|
||||
apiCall.setResponseHeaders(request.getResponseHeaders());
|
||||
apiCall.setPollCount(request.getPollCount());
|
||||
apiCall.setFinalStatus(request.getFinalStatus());
|
||||
apiCall.setStatus(request.getStatus());
|
||||
apiCall.setDurationMs(request.getDurationMs());
|
||||
apiCall.setPromptTokens(request.getPromptTokens());
|
||||
apiCall.setCompletionTokens(request.getCompletionTokens());
|
||||
apiCall.setTotalTokens(request.getTotalTokens());
|
||||
apiCall.setCost(request.getCost());
|
||||
apiCall.setFunctionCalls(request.getFunctionCalls());
|
||||
apiCall.setFunctionResults(request.getFunctionResults());
|
||||
apiCall.setErrorCode(request.getErrorCode());
|
||||
apiCall.setErrorMessage(request.getErrorMessage());
|
||||
apiCall.setClientIp(request.getClientIp());
|
||||
apiCall.setUserAgent(request.getUserAgent());
|
||||
apiCall.setSessionId(request.getSessionId());
|
||||
apiCall.setTraceId(request.getTraceId());
|
||||
apiCall.setMetadata(request.getMetadata());
|
||||
|
||||
this.save(apiCall);
|
||||
return convertToResponse(apiCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCallResponse update(CozeApiCallUpdateRequest request) {
|
||||
CozeApiCall apiCall = this.getBaseMapper().selectById(request.getId());
|
||||
if (apiCall == null || apiCall.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (request.getConversationId() != null) {
|
||||
apiCall.setConversationId(request.getConversationId());
|
||||
}
|
||||
if (request.getMessageId() != null) {
|
||||
apiCall.setMessageId(request.getMessageId());
|
||||
}
|
||||
if (request.getCozeChatId() != null) {
|
||||
apiCall.setCozeChatId(request.getCozeChatId());
|
||||
}
|
||||
if (request.getCozeConversationId() != null) {
|
||||
apiCall.setCozeConversationId(request.getCozeConversationId());
|
||||
}
|
||||
if (request.getBotId() != null) {
|
||||
apiCall.setBotId(request.getBotId());
|
||||
}
|
||||
if (request.getWorkflowId() != null) {
|
||||
apiCall.setWorkflowId(request.getWorkflowId());
|
||||
}
|
||||
if (request.getUserId() != null) {
|
||||
apiCall.setUserId(request.getUserId());
|
||||
}
|
||||
if (request.getRequestType() != null) {
|
||||
apiCall.setRequestType(request.getRequestType());
|
||||
}
|
||||
if (request.getRequestUrl() != null) {
|
||||
apiCall.setRequestUrl(request.getRequestUrl());
|
||||
}
|
||||
if (request.getRequestBody() != null) {
|
||||
apiCall.setRequestBody(request.getRequestBody());
|
||||
}
|
||||
if (request.getRequestHeaders() != null) {
|
||||
apiCall.setRequestHeaders(request.getRequestHeaders());
|
||||
}
|
||||
if (request.getUserMessage() != null) {
|
||||
apiCall.setUserMessage(request.getUserMessage());
|
||||
}
|
||||
if (request.getUserMessageType() != null) {
|
||||
apiCall.setUserMessageType(request.getUserMessageType());
|
||||
}
|
||||
if (request.getAiReply() != null) {
|
||||
apiCall.setAiReply(request.getAiReply());
|
||||
}
|
||||
if (request.getAiReplyType() != null) {
|
||||
apiCall.setAiReplyType(request.getAiReplyType());
|
||||
}
|
||||
if (request.getResponseStatus() != null) {
|
||||
apiCall.setResponseStatus(request.getResponseStatus());
|
||||
}
|
||||
if (request.getResponseBody() != null) {
|
||||
apiCall.setResponseBody(request.getResponseBody());
|
||||
}
|
||||
if (request.getResponseHeaders() != null) {
|
||||
apiCall.setResponseHeaders(request.getResponseHeaders());
|
||||
}
|
||||
if (request.getPollCount() != null) {
|
||||
apiCall.setPollCount(request.getPollCount());
|
||||
}
|
||||
if (request.getFinalStatus() != null) {
|
||||
apiCall.setFinalStatus(request.getFinalStatus());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
apiCall.setStatus(request.getStatus());
|
||||
}
|
||||
if (request.getDurationMs() != null) {
|
||||
apiCall.setDurationMs(request.getDurationMs());
|
||||
}
|
||||
if (request.getPromptTokens() != null) {
|
||||
apiCall.setPromptTokens(request.getPromptTokens());
|
||||
}
|
||||
if (request.getCompletionTokens() != null) {
|
||||
apiCall.setCompletionTokens(request.getCompletionTokens());
|
||||
}
|
||||
if (request.getTotalTokens() != null) {
|
||||
apiCall.setTotalTokens(request.getTotalTokens());
|
||||
}
|
||||
if (request.getCost() != null) {
|
||||
apiCall.setCost(request.getCost());
|
||||
}
|
||||
if (request.getFunctionCalls() != null) {
|
||||
apiCall.setFunctionCalls(request.getFunctionCalls());
|
||||
}
|
||||
if (request.getFunctionResults() != null) {
|
||||
apiCall.setFunctionResults(request.getFunctionResults());
|
||||
}
|
||||
if (request.getErrorCode() != null) {
|
||||
apiCall.setErrorCode(request.getErrorCode());
|
||||
}
|
||||
if (request.getErrorMessage() != null) {
|
||||
apiCall.setErrorMessage(request.getErrorMessage());
|
||||
}
|
||||
if (request.getClientIp() != null) {
|
||||
apiCall.setClientIp(request.getClientIp());
|
||||
}
|
||||
if (request.getUserAgent() != null) {
|
||||
apiCall.setUserAgent(request.getUserAgent());
|
||||
}
|
||||
if (request.getSessionId() != null) {
|
||||
apiCall.setSessionId(request.getSessionId());
|
||||
}
|
||||
if (request.getTraceId() != null) {
|
||||
apiCall.setTraceId(request.getTraceId());
|
||||
}
|
||||
if (request.getMetadata() != null) {
|
||||
apiCall.setMetadata(request.getMetadata());
|
||||
}
|
||||
|
||||
this.updateById(apiCall);
|
||||
return convertToResponse(apiCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String id) {
|
||||
CozeApiCall apiCall = this.getBaseMapper().selectById(id);
|
||||
if (apiCall == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserId(String userId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByBotId(String botId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getBotId, botId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByStatus(String status) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getStatus, status)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long sumTokensByUserId(String userId) {
|
||||
// 使用原生SQL或者查询后计算
|
||||
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.isNotNull(CozeApiCall::getTotalTokens));
|
||||
return calls.stream().mapToLong(call -> call.getTotalTokens() != null ? call.getTotalTokens() : 0).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal sumCostByUserId(String userId) {
|
||||
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.isNotNull(CozeApiCall::getCost));
|
||||
return calls.stream()
|
||||
.map(call -> call.getCost() != null ? call.getCost() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private CozeApiCallResponse convertToResponse(CozeApiCall apiCall) {
|
||||
CozeApiCallResponse response = new CozeApiCallResponse();
|
||||
BeanUtils.copyProperties(apiCall, response);
|
||||
response.setId(apiCall.getId());
|
||||
if (apiCall.getCreateTime() != null) {
|
||||
response.setCreateTime(apiCall.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (apiCall.getUpdateTime() != null) {
|
||||
response.setUpdateTime(apiCall.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (apiCall.getStartTime() != null) {
|
||||
response.setStartTime(apiCall.getStartTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (apiCall.getEndTime() != null) {
|
||||
response.setEndTime(apiCall.getEndTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (apiCall.getPollStartTime() != null) {
|
||||
response.setPollStartTime(apiCall.getPollStartTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (apiCall.getPollEndTime() != null) {
|
||||
response.setPollEndTime(apiCall.getPollEndTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页对象为PageResult对象
|
||||
*/
|
||||
private PageResult<CozeApiCallResponse> convertPageToPageResult(Page<CozeApiCall> page) {
|
||||
PageResult<CozeApiCallResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList()));
|
||||
return pageResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.emotion.dto.request.AiConfigCallStatsRequest;
|
||||
import com.emotion.dto.response.AiConfigCallStatsResponse;
|
||||
import com.emotion.dto.response.AiConfigCallStatsItem;
|
||||
import com.emotion.dto.response.DashboardStatsResponse;
|
||||
import com.emotion.entity.*;
|
||||
import com.emotion.mapper.AiConfigMapper;
|
||||
import com.emotion.service.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 仪表盘服务实现类
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-10-31
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DashboardServiceImpl implements DashboardService {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private GuestUserService guestUserService;
|
||||
|
||||
@Autowired
|
||||
private ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private DiaryPostService diaryPostService;
|
||||
|
||||
@Autowired
|
||||
private CommunityPostService communityPostService;
|
||||
|
||||
@Autowired
|
||||
private EmotionRecordService emotionRecordService;
|
||||
|
||||
@Autowired
|
||||
private CozeApiCallService cozeApiCallService;
|
||||
|
||||
@Autowired
|
||||
private AiConfigService aiConfigService;
|
||||
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
@Autowired
|
||||
private AiConfigMapper aiConfigMapper;
|
||||
|
||||
@Autowired
|
||||
private AchievementService achievementService;
|
||||
|
||||
@Autowired
|
||||
private RewardService rewardService;
|
||||
|
||||
@Override
|
||||
public DashboardStatsResponse getDashboardStats() {
|
||||
log.info("获取仪表盘统计数据");
|
||||
|
||||
try {
|
||||
return DashboardStatsResponse.builder()
|
||||
.userStats(getUserStats())
|
||||
.contentStats(getContentStats())
|
||||
.aiServiceStats(getAiServiceStats())
|
||||
.systemStats(getSystemStats())
|
||||
.recentActivities(getRecentActivities())
|
||||
.userGrowthTrends(getUserGrowthTrends(7))
|
||||
.recentLogins(getRecentLogins(10))
|
||||
.updateTime(LocalDateTime.now())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("获取仪表盘统计数据失败", e);
|
||||
// 返回默认数据,避免前端报错
|
||||
return getDefaultDashboardStats();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardStatsResponse.UserStats getUserStats() {
|
||||
try {
|
||||
// 获取用户统计数据
|
||||
Long totalUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0));
|
||||
|
||||
Long guestUsers = guestUserService.count(new LambdaQueryWrapper<GuestUser>()
|
||||
.eq(GuestUser::getIsDeleted, 0));
|
||||
|
||||
// 获取今日新增用户
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
Long todayNewUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.ge(User::getCreateTime, todayStart)
|
||||
.lt(User::getCreateTime, todayEnd));
|
||||
|
||||
// 活跃用户数(最近7天有活动的用户)
|
||||
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
|
||||
Long activeUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.ge(User::getLastActiveTime, sevenDaysAgo));
|
||||
|
||||
return DashboardStatsResponse.UserStats.builder()
|
||||
.totalUsers(totalUsers)
|
||||
.todayNewUsers(todayNewUsers)
|
||||
.activeUsers(activeUsers)
|
||||
.guestUsers(guestUsers)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户统计数据失败", e);
|
||||
return DashboardStatsResponse.UserStats.builder()
|
||||
.totalUsers(0L)
|
||||
.todayNewUsers(0L)
|
||||
.activeUsers(0L)
|
||||
.guestUsers(0L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardStatsResponse.ContentStats getContentStats() {
|
||||
try {
|
||||
Long totalConversations = conversationService.count(new LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getIsDeleted, 0));
|
||||
|
||||
Long totalMessages = messageService.count(new LambdaQueryWrapper<Message>()
|
||||
.eq(Message::getIsDeleted, 0));
|
||||
|
||||
Long diaryPosts = diaryPostService.count(new LambdaQueryWrapper<DiaryPost>()
|
||||
.eq(DiaryPost::getIsDeleted, 0));
|
||||
|
||||
Long communityPosts = communityPostService.count(new LambdaQueryWrapper<CommunityPost>()
|
||||
.eq(CommunityPost::getIsDeleted, 0));
|
||||
|
||||
Long emotionRecords = emotionRecordService.count(new LambdaQueryWrapper<EmotionRecord>()
|
||||
.eq(EmotionRecord::getIsDeleted, 0));
|
||||
|
||||
return DashboardStatsResponse.ContentStats.builder()
|
||||
.totalConversations(totalConversations)
|
||||
.totalMessages(totalMessages)
|
||||
.diaryPosts(diaryPosts)
|
||||
.communityPosts(communityPosts)
|
||||
.emotionRecords(emotionRecords)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("获取内容统计数据失败", e);
|
||||
return DashboardStatsResponse.ContentStats.builder()
|
||||
.totalConversations(0L)
|
||||
.totalMessages(0L)
|
||||
.diaryPosts(0L)
|
||||
.communityPosts(0L)
|
||||
.emotionRecords(0L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardStatsResponse.AiServiceStats getAiServiceStats() {
|
||||
try {
|
||||
Long totalApiCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getIsDeleted, 0));
|
||||
|
||||
Long aiConfigCount = aiConfigService.count(new LambdaQueryWrapper<AiConfig>()
|
||||
.eq(AiConfig::getIsDeleted, 0));
|
||||
|
||||
// 获取今日API调用次数
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
Long todayApiCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.ge(CozeApiCall::getCreateTime, todayStart)
|
||||
.lt(CozeApiCall::getCreateTime, todayEnd));
|
||||
|
||||
// 获取成功和失败的调用次数
|
||||
Long successfulCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.eq(CozeApiCall::getStatus, "success"));
|
||||
|
||||
Long failedCalls = cozeApiCallService.count(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.eq(CozeApiCall::getStatus, "failed"));
|
||||
|
||||
// 计算平均响应时间(从duration_ms字段)
|
||||
List<CozeApiCall> apiCalls = cozeApiCallService.list(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.isNotNull(CozeApiCall::getDurationMs)
|
||||
.last("LIMIT 1000")); // 取最近1000条记录计算平均值
|
||||
|
||||
Double avgResponseTime = apiCalls.stream()
|
||||
.filter(call -> call.getDurationMs() != null)
|
||||
.mapToInt(CozeApiCall::getDurationMs)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
return DashboardStatsResponse.AiServiceStats.builder()
|
||||
.totalApiCalls(totalApiCalls)
|
||||
.todayApiCalls(todayApiCalls)
|
||||
.successfulCalls(successfulCalls)
|
||||
.failedCalls(failedCalls)
|
||||
.avgResponseTime(avgResponseTime)
|
||||
.aiConfigCount(aiConfigCount)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("获取AI服务统计数据失败", e);
|
||||
return DashboardStatsResponse.AiServiceStats.builder()
|
||||
.totalApiCalls(0L)
|
||||
.todayApiCalls(0L)
|
||||
.successfulCalls(0L)
|
||||
.failedCalls(0L)
|
||||
.avgResponseTime(0.0)
|
||||
.aiConfigCount(0L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardStatsResponse.SystemStats getSystemStats() {
|
||||
try {
|
||||
Long adminCount = adminService.count(new LambdaQueryWrapper<Admin>()
|
||||
.eq(Admin::getIsDeleted, 0)
|
||||
.eq(Admin::getStatus, 1)); // 只统计正常状态的管理员
|
||||
|
||||
Long achievementCount = achievementService.count(new LambdaQueryWrapper<Achievement>()
|
||||
.eq(Achievement::getIsDeleted, 0));
|
||||
|
||||
Long rewardCount = rewardService.count(new LambdaQueryWrapper<Reward>()
|
||||
.eq(Reward::getIsDeleted, 0));
|
||||
|
||||
// 获取系统运行时间
|
||||
long uptimeMs = ManagementFactory.getRuntimeMXBean().getUptime();
|
||||
String uptime = formatUptime(uptimeMs);
|
||||
|
||||
return DashboardStatsResponse.SystemStats.builder()
|
||||
.adminCount(adminCount)
|
||||
.achievementCount(achievementCount)
|
||||
.rewardCount(rewardCount)
|
||||
.uptime(uptime)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统统计数据失败", e);
|
||||
return DashboardStatsResponse.SystemStats.builder()
|
||||
.adminCount(0L)
|
||||
.achievementCount(0L)
|
||||
.rewardCount(0L)
|
||||
.uptime("未知")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近活动
|
||||
*/
|
||||
private List<DashboardStatsResponse.RecentActivity> getRecentActivities() {
|
||||
List<DashboardStatsResponse.RecentActivity> activities = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 获取最近注册的用户
|
||||
List<User> recentUsers = userService.list(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByDesc(User::getCreateTime)
|
||||
.last("LIMIT 3"));
|
||||
|
||||
for (User user : recentUsers) {
|
||||
activities.add(DashboardStatsResponse.RecentActivity.builder()
|
||||
.type("user_register")
|
||||
.description("新用户注册: " + (user.getNickname() != null ? user.getNickname() : user.getUsername()))
|
||||
.userId(user.getId())
|
||||
.username(user.getNickname() != null ? user.getNickname() : user.getUsername())
|
||||
.activityTime(user.getCreateTime())
|
||||
.extraData(new HashMap<>())
|
||||
.build());
|
||||
}
|
||||
|
||||
// 获取最近的对话
|
||||
List<Conversation> recentConversations = conversationService.list(new LambdaQueryWrapper<Conversation>()
|
||||
.eq(Conversation::getIsDeleted, 0)
|
||||
.orderByDesc(Conversation::getCreateTime)
|
||||
.last("LIMIT 3"));
|
||||
|
||||
for (Conversation conversation : recentConversations) {
|
||||
Map<String, Object> extraData = new HashMap<>();
|
||||
extraData.put("conversationId", conversation.getId());
|
||||
extraData.put("title", conversation.getTitle());
|
||||
|
||||
activities.add(DashboardStatsResponse.RecentActivity.builder()
|
||||
.type("ai_chat")
|
||||
.description("新的AI对话: " + (conversation.getTitle() != null ? conversation.getTitle() : "未命名对话"))
|
||||
.userId(conversation.getUserId())
|
||||
.username("用户")
|
||||
.activityTime(conversation.getCreateTime())
|
||||
.extraData(extraData)
|
||||
.build());
|
||||
}
|
||||
|
||||
// 获取最近的日记帖子
|
||||
List<DiaryPost> recentDiaryPosts = diaryPostService.list(new LambdaQueryWrapper<DiaryPost>()
|
||||
.eq(DiaryPost::getIsDeleted, 0)
|
||||
.orderByDesc(DiaryPost::getCreateTime)
|
||||
.last("LIMIT 2"));
|
||||
|
||||
for (DiaryPost post : recentDiaryPosts) {
|
||||
Map<String, Object> extraData = new HashMap<>();
|
||||
extraData.put("postId", post.getId());
|
||||
extraData.put("title", post.getTitle());
|
||||
|
||||
activities.add(DashboardStatsResponse.RecentActivity.builder()
|
||||
.type("diary_post")
|
||||
.description("新的日记: " + (post.getTitle() != null ? post.getTitle() : "无标题"))
|
||||
.userId(post.getUserId())
|
||||
.username("用户")
|
||||
.activityTime(post.getCreateTime())
|
||||
.extraData(extraData)
|
||||
.build());
|
||||
}
|
||||
|
||||
// 按时间排序,取最新的10条
|
||||
activities.sort((a, b) -> b.getActivityTime().compareTo(a.getActivityTime()));
|
||||
if (activities.size() > 10) {
|
||||
activities = activities.subList(0, 10);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近活动失败", e);
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化运行时间
|
||||
*/
|
||||
private String formatUptime(long uptimeMs) {
|
||||
long seconds = uptimeMs / 1000;
|
||||
long minutes = seconds / 60;
|
||||
long hours = minutes / 60;
|
||||
long days = hours / 24;
|
||||
|
||||
if (days > 0) {
|
||||
return String.format("%d天%d小时%d分钟", days, hours % 24, minutes % 60);
|
||||
} else if (hours > 0) {
|
||||
return String.format("%d小时%d分钟", hours, minutes % 60);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%d分钟", minutes);
|
||||
} else {
|
||||
return String.format("%d秒", seconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认仪表盘统计数据(用于异常情况)
|
||||
*/
|
||||
private DashboardStatsResponse getDefaultDashboardStats() {
|
||||
return DashboardStatsResponse.builder()
|
||||
.userStats(DashboardStatsResponse.UserStats.builder()
|
||||
.totalUsers(0L)
|
||||
.todayNewUsers(0L)
|
||||
.activeUsers(0L)
|
||||
.guestUsers(0L)
|
||||
.build())
|
||||
.contentStats(DashboardStatsResponse.ContentStats.builder()
|
||||
.totalConversations(0L)
|
||||
.totalMessages(0L)
|
||||
.diaryPosts(0L)
|
||||
.communityPosts(0L)
|
||||
.emotionRecords(0L)
|
||||
.build())
|
||||
.aiServiceStats(DashboardStatsResponse.AiServiceStats.builder()
|
||||
.totalApiCalls(0L)
|
||||
.todayApiCalls(0L)
|
||||
.successfulCalls(0L)
|
||||
.failedCalls(0L)
|
||||
.avgResponseTime(0.0)
|
||||
.aiConfigCount(0L)
|
||||
.build())
|
||||
.systemStats(DashboardStatsResponse.SystemStats.builder()
|
||||
.adminCount(0L)
|
||||
.achievementCount(0L)
|
||||
.rewardCount(0L)
|
||||
.uptime("未知")
|
||||
.build())
|
||||
.recentActivities(new ArrayList<>())
|
||||
.userGrowthTrends(new ArrayList<>())
|
||||
.recentLogins(new ArrayList<>())
|
||||
.updateTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DashboardStatsResponse.UserGrowthTrend> getUserGrowthTrends(int days) {
|
||||
List<DashboardStatsResponse.UserGrowthTrend> trends = new ArrayList<>();
|
||||
|
||||
try {
|
||||
LocalDate endDate = LocalDate.now();
|
||||
LocalDate startDate = endDate.minusDays(days - 1);
|
||||
|
||||
// 获取指定日期范围内每天的用户注册数据
|
||||
for (int i = 0; i < days; i++) {
|
||||
LocalDate currentDate = startDate.plusDays(i);
|
||||
LocalDateTime dayStart = currentDate.atStartOfDay();
|
||||
LocalDateTime dayEnd = dayStart.plusDays(1);
|
||||
|
||||
// 统计当天新增用户数
|
||||
Long newUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.ge(User::getCreateTime, dayStart)
|
||||
.lt(User::getCreateTime, dayEnd));
|
||||
|
||||
// 统计截止到当天的总用户数
|
||||
Long totalUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.lt(User::getCreateTime, dayEnd));
|
||||
|
||||
trends.add(DashboardStatsResponse.UserGrowthTrend.builder()
|
||||
.date(currentDate.toString())
|
||||
.newUsers(newUsers)
|
||||
.totalUsers(totalUsers)
|
||||
.build());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户增长趋势数据失败", e);
|
||||
}
|
||||
|
||||
return trends;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DashboardStatsResponse.RecentLogin> getRecentLogins(int limit) {
|
||||
List<DashboardStatsResponse.RecentLogin> recentLogins = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 获取最近活跃的用户(按最后活跃时间排序)
|
||||
List<User> recentUsers = userService.list(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.isNotNull(User::getLastActiveTime)
|
||||
.orderByDesc(User::getLastActiveTime)
|
||||
.last("LIMIT " + limit));
|
||||
|
||||
for (User user : recentUsers) {
|
||||
String timeDescription = formatTimeDescription(user.getLastActiveTime());
|
||||
|
||||
recentLogins.add(DashboardStatsResponse.RecentLogin.builder()
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.nickname(user.getNickname())
|
||||
.avatar(user.getAvatar())
|
||||
.loginTime(user.getLastActiveTime())
|
||||
.timeDescription(timeDescription)
|
||||
.build());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近登录用户失败", e);
|
||||
}
|
||||
|
||||
return recentLogins;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConfigCallStatsResponse getAiConfigCallStats(AiConfigCallStatsRequest request) {
|
||||
Integer limit = request != null ? request.getLimit() : null;
|
||||
if (limit == null) {
|
||||
limit = 20;
|
||||
}
|
||||
// 防御性限制,避免过大查询
|
||||
if (limit > 200) {
|
||||
limit = 200;
|
||||
}
|
||||
if (limit < 1) {
|
||||
limit = 1;
|
||||
}
|
||||
|
||||
List<AiConfigCallStatsItem> items = aiConfigMapper.selectAiConfigCallStats(limit);
|
||||
return AiConfigCallStatsResponse.builder()
|
||||
.items(items)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间描述
|
||||
*/
|
||||
private String formatTimeDescription(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return "未知";
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long minutes = java.time.Duration.between(dateTime, now).toMinutes();
|
||||
|
||||
if (minutes < 1) {
|
||||
return "刚刚";
|
||||
} else if (minutes < 60) {
|
||||
return minutes + "分钟前";
|
||||
} else if (minutes < 1440) { // 24小时
|
||||
long hours = minutes / 60;
|
||||
return hours + "小时前";
|
||||
} else {
|
||||
long days = minutes / 1440;
|
||||
if (days == 1) {
|
||||
return "昨天";
|
||||
} else if (days < 7) {
|
||||
return days + "天前";
|
||||
} else {
|
||||
return dateTime.toLocalDate().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.DiaryCommentCreateRequest;
|
||||
import com.emotion.dto.request.DiaryCommentPageRequest;
|
||||
import com.emotion.dto.response.DiaryCommentResponse;
|
||||
import com.emotion.entity.DiaryComment;
|
||||
import com.emotion.mapper.DiaryCommentMapper;
|
||||
import com.emotion.service.DiaryCommentService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 日记评论服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class DiaryCommentServiceImpl extends ServiceImpl<DiaryCommentMapper, DiaryComment> implements DiaryCommentService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<DiaryCommentResponse> getPageWithResponse(DiaryCommentPageRequest request) {
|
||||
Page<DiaryComment> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 基础查询条件
|
||||
wrapper.eq(DiaryComment::getIsDeleted, 0);
|
||||
|
||||
// 根据请求参数添加查询条件
|
||||
if (StringUtils.hasText(request.getDiaryId())) {
|
||||
wrapper.eq(DiaryComment::getDiaryId, request.getDiaryId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(DiaryComment::getUserId, request.getUserId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getCommentType())) {
|
||||
wrapper.eq(DiaryComment::getCommentType, request.getCommentType());
|
||||
}
|
||||
if (request.getTopLevelOnly() != null && request.getTopLevelOnly()) {
|
||||
wrapper.isNull(DiaryComment::getParentCommentId);
|
||||
}
|
||||
|
||||
// 排序
|
||||
wrapper.orderByDesc(DiaryComment::getIsTop)
|
||||
.orderByDesc(DiaryComment::getPublishTime);
|
||||
|
||||
IPage<DiaryComment> resultPage = this.page(page, wrapper);
|
||||
return convertPageToResponse(resultPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryCommentResponse getCommentResponseById(String id) {
|
||||
DiaryComment comment = this.getById(id);
|
||||
if (comment == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiaryCommentResponse> getCommentTreeWithResponse(String diaryId) {
|
||||
List<DiaryComment> comments = this.getCommentTree(diaryId);
|
||||
return comments.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryCommentResponse createCommentWithResponse(DiaryCommentCreateRequest request) {
|
||||
DiaryComment comment = DiaryComment.builder()
|
||||
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
|
||||
.diaryId(request.getDiaryId())
|
||||
.userId(request.getUserId())
|
||||
.content(request.getContent())
|
||||
.images(convertListToJson(request.getImages()))
|
||||
.parentCommentId(request.getParentCommentId())
|
||||
.commentType("user")
|
||||
.likeCount(0)
|
||||
.replyCount(0)
|
||||
.isAnonymous(request.getIsAnonymous())
|
||||
.isTop(0)
|
||||
.status("published")
|
||||
.publishTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
this.save(comment);
|
||||
|
||||
// 如果有父评论,更新父评论的回复数
|
||||
if (StringUtils.hasText(request.getParentCommentId())) {
|
||||
this.incrementReplyCount(request.getParentCommentId());
|
||||
this.updateLastReplyTime(request.getParentCommentId());
|
||||
}
|
||||
|
||||
return convertToResponse(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryCommentResponse updateCommentWithResponse(DiaryCommentCreateRequest request) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, request.getId())
|
||||
.set(StringUtils.hasText(request.getContent()), DiaryComment::getContent, request.getContent())
|
||||
.set(request.getImages() != null, DiaryComment::getImages, convertListToJson(request.getImages()))
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
|
||||
boolean updated = this.update(wrapper);
|
||||
if (!updated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DiaryComment updatedComment = this.getById(request.getId());
|
||||
return updatedComment != null ? convertToResponse(updatedComment) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteComment(String commentId) {
|
||||
return this.removeById(commentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean softDeleteComment(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.set(DiaryComment::getIsDeleted, 1)
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean restoreComment(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.set(DiaryComment::getIsDeleted, 0)
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementLikeCount(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.setSql("like_count = like_count + 1")
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean decrementLikeCount(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.setSql("like_count = GREATEST(like_count - 1, 0)")
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setTop(String commentId, Integer isTop) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.set(DiaryComment::getIsTop, isTop)
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论树结构
|
||||
*/
|
||||
private List<DiaryComment> getCommentTree(String diaryId) {
|
||||
// 获取所有顶级评论(没有父评论的评论)
|
||||
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DiaryComment::getDiaryId, diaryId)
|
||||
.isNull(DiaryComment::getParentCommentId)
|
||||
.eq(DiaryComment::getIsDeleted, 0)
|
||||
.orderByDesc(DiaryComment::getIsTop)
|
||||
.orderByDesc(DiaryComment::getPublishTime);
|
||||
|
||||
List<DiaryComment> topComments = this.list(wrapper);
|
||||
|
||||
// 为每个顶级评论加载回复
|
||||
return topComments.stream()
|
||||
.peek(comment -> {
|
||||
List<DiaryComment> replies = getRepliesByParentId(comment.getId());
|
||||
// 这里可以递归加载更深层的回复,但为了性能考虑,通常只加载一层
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父评论ID查询回复列表
|
||||
*/
|
||||
private List<DiaryComment> getRepliesByParentId(String parentCommentId) {
|
||||
LambdaQueryWrapper<DiaryComment> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DiaryComment::getParentCommentId, parentCommentId)
|
||||
.eq(DiaryComment::getIsDeleted, 0)
|
||||
.orderByAsc(DiaryComment::getPublishTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加回复数
|
||||
*/
|
||||
private boolean incrementReplyCount(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.setSql("reply_count = reply_count + 1")
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少回复数
|
||||
*/
|
||||
private boolean decrementReplyCount(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.setSql("reply_count = GREATEST(reply_count - 1, 0)")
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新最后回复时间
|
||||
*/
|
||||
private boolean updateLastReplyTime(String commentId) {
|
||||
LambdaUpdateWrapper<DiaryComment> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryComment::getId, commentId)
|
||||
.set(DiaryComment::getLastReplyTime, LocalDateTime.now())
|
||||
.set(DiaryComment::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private DiaryCommentResponse convertToResponse(DiaryComment comment) {
|
||||
DiaryCommentResponse response = new DiaryCommentResponse();
|
||||
BeanUtils.copyProperties(comment, response);
|
||||
|
||||
// 转换时间格式
|
||||
if (comment.getPublishTime() != null) {
|
||||
response.setPublishTime(comment.getPublishTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (comment.getLastReplyTime() != null) {
|
||||
response.setLastReplyTime(comment.getLastReplyTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (comment.getCreateTime() != null) {
|
||||
response.setCreateTime(comment.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (comment.getUpdateTime() != null) {
|
||||
response.setUpdateTime(comment.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
|
||||
// 转换JSON字段
|
||||
try {
|
||||
if (comment.getImages() != null) {
|
||||
response.setImages(objectMapper.readValue(comment.getImages(), new TypeReference<List<String>>() {
|
||||
}));
|
||||
}
|
||||
if (comment.getMetadata() != null) {
|
||||
response.setMetadata(objectMapper.readValue(comment.getMetadata(), Object.class));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
// 忽略JSON解析错误
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页对象为响应对象
|
||||
*/
|
||||
private PageResult<DiaryCommentResponse> convertPageToResponse(IPage<DiaryComment> page) {
|
||||
PageResult<DiaryCommentResponse> responsePage = new PageResult<>();
|
||||
responsePage.setCurrent(page.getCurrent());
|
||||
responsePage.setSize(page.getSize());
|
||||
responsePage.setTotal(page.getTotal());
|
||||
responsePage.setPages(page.getPages());
|
||||
responsePage.setRecords(
|
||||
page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList()));
|
||||
return responsePage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将List转换为JSON字符串
|
||||
*/
|
||||
private String convertListToJson(List<?> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(list);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.DiaryPostCreateRequest;
|
||||
import com.emotion.dto.request.DiaryPostPageRequest;
|
||||
import com.emotion.dto.request.DiaryPostUpdateRequest;
|
||||
import com.emotion.dto.response.DiaryPostResponse;
|
||||
import com.emotion.entity.DiaryPost;
|
||||
import com.emotion.mapper.DiaryPostMapper;
|
||||
import com.emotion.service.DiaryPostService;
|
||||
import com.emotion.service.DiaryCommentService;
|
||||
import com.emotion.service.AiChatService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户日记服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class DiaryPostServiceImpl extends ServiceImpl<DiaryPostMapper, DiaryPost> implements DiaryPostService {
|
||||
|
||||
@Autowired
|
||||
private AiChatService aiChatService;
|
||||
@Autowired
|
||||
private DiaryCommentService diaryCommentService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<DiaryPostResponse> getPageWithResponse(DiaryPostPageRequest request) {
|
||||
Page<DiaryPost> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 基础查询条件
|
||||
wrapper.eq(DiaryPost::getIsDeleted, 0);
|
||||
|
||||
// 根据请求参数添加查询条件
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(DiaryPost::getUserId, request.getUserId());
|
||||
}
|
||||
if (Boolean.TRUE.equals(request.getPublicOnly())) {
|
||||
wrapper.eq(DiaryPost::getIsPublic, 1)
|
||||
.eq(DiaryPost::getStatus, "published");
|
||||
}
|
||||
if (Boolean.TRUE.equals(request.getFeaturedOnly())) {
|
||||
wrapper.eq(DiaryPost::getFeatured, 1)
|
||||
.eq(DiaryPost::getIsPublic, 1)
|
||||
.eq(DiaryPost::getStatus, "published");
|
||||
}
|
||||
if (StringUtils.hasText(request.getMood())) {
|
||||
wrapper.eq(DiaryPost::getMood, request.getMood());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTag())) {
|
||||
// 标签查询可能需要特殊处理,因为tags字段是JSON格式
|
||||
wrapper.like(DiaryPost::getTags, request.getTag());
|
||||
}
|
||||
|
||||
// 排序
|
||||
wrapper.orderByDesc(DiaryPost::getPriority)
|
||||
.orderByDesc(DiaryPost::getPublishTime);
|
||||
|
||||
IPage<DiaryPost> resultPage = this.page(page, wrapper);
|
||||
List<DiaryPostResponse> responses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
return createPageResult(resultPage, responses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryPostResponse getDiaryPostResponseById(String id) {
|
||||
DiaryPost diaryPost = this.getById(id);
|
||||
if (diaryPost == null) {
|
||||
return null;
|
||||
}
|
||||
// 增加浏览数
|
||||
incrementViewCount(id);
|
||||
return convertToResponse(diaryPost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryPostResponse createDiaryPostWithResponse(DiaryPostCreateRequest request) {
|
||||
// 处理标题:如果为空,则设置为null或生成默认标题
|
||||
String title = request.getTitle();
|
||||
if (title == null || title.trim().isEmpty()) {
|
||||
// 可以选择设置为null,或者生成一个默认标题
|
||||
// 这里我们设置为null,让数据库使用默认值
|
||||
title = null;
|
||||
}
|
||||
|
||||
DiaryPost diaryPost = DiaryPost.builder()
|
||||
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
|
||||
.userId(request.getUserId())
|
||||
.title(title)
|
||||
.content(request.getContent())
|
||||
.images(convertListToJson(request.getImages()))
|
||||
.videos(convertListToJson(request.getVideos()))
|
||||
.location(request.getLocation())
|
||||
.weather(request.getWeather())
|
||||
.mood(request.getMood())
|
||||
.tags(convertListToJson(request.getTags()))
|
||||
.isPublic(request.getIsPublic())
|
||||
.isAnonymous(request.getIsAnonymous())
|
||||
.viewCount(0)
|
||||
.likeCount(0)
|
||||
.commentCount(0)
|
||||
.shareCount(0)
|
||||
.publishTime(LocalDateTime.now())
|
||||
.status("published")
|
||||
.priority(0)
|
||||
.featured(0)
|
||||
.build();
|
||||
|
||||
this.save(diaryPost);
|
||||
return convertToResponse(diaryPost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryPostResponse updateDiaryPostWithResponse(DiaryPostUpdateRequest request) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, request.getId())
|
||||
.set(StringUtils.hasText(request.getTitle()), DiaryPost::getTitle, request.getTitle())
|
||||
.set(StringUtils.hasText(request.getContent()), DiaryPost::getContent, request.getContent())
|
||||
.set(request.getImages() != null, DiaryPost::getImages, convertListToJson(request.getImages()))
|
||||
.set(request.getVideos() != null, DiaryPost::getVideos, convertListToJson(request.getVideos()))
|
||||
.set(StringUtils.hasText(request.getLocation()), DiaryPost::getLocation, request.getLocation())
|
||||
.set(StringUtils.hasText(request.getWeather()), DiaryPost::getWeather, request.getWeather())
|
||||
.set(StringUtils.hasText(request.getMood()), DiaryPost::getMood, request.getMood())
|
||||
.set(request.getTags() != null, DiaryPost::getTags, convertListToJson(request.getTags()))
|
||||
.set(request.getIsPublic() != null, DiaryPost::getIsPublic, request.getIsPublic())
|
||||
.set(request.getIsAnonymous() != null, DiaryPost::getIsAnonymous, request.getIsAnonymous())
|
||||
.set(StringUtils.hasText(request.getStatus()), DiaryPost::getStatus, request.getStatus())
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
|
||||
boolean updated = this.update(wrapper);
|
||||
if (!updated) {
|
||||
return null;
|
||||
}
|
||||
DiaryPost updatedDiaryPost = this.getById(request.getId());
|
||||
return convertToResponse(updatedDiaryPost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteDiaryPost(String diaryId) {
|
||||
return this.removeById(diaryId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean softDeleteDiaryPost(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getIsDeleted, 1)
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean restoreDiaryPost(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getIsDeleted, 0)
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementViewCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("view_count = view_count + 1")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementLikeCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("like_count = like_count + 1")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean decrementLikeCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("like_count = GREATEST(like_count - 1, 0)")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementCommentCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("comment_count = comment_count + 1")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean decrementCommentCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("comment_count = GREATEST(comment_count - 1, 0)")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementShareCount(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.setSql("share_count = share_count + 1")
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateLastCommentTime(String diaryId) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getLastCommentTime, LocalDateTime.now())
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setFeatured(String diaryId, Integer featured) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getFeatured, featured)
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setPriority(String diaryId, Integer priority) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getPriority, priority)
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserId(String userId) {
|
||||
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DiaryPost::getUserId, userId)
|
||||
.eq(DiaryPost::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countPublicByUserId(String userId) {
|
||||
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DiaryPost::getUserId, userId)
|
||||
.eq(DiaryPost::getIsPublic, 1)
|
||||
.eq(DiaryPost::getStatus, "published")
|
||||
.eq(DiaryPost::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countFeatured() {
|
||||
LambdaQueryWrapper<DiaryPost> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DiaryPost::getFeatured, 1)
|
||||
.eq(DiaryPost::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiaryPostResponse publishDiaryWithAiComment(DiaryPostCreateRequest request) {
|
||||
// 1. 保存日记
|
||||
DiaryPost diaryPost = DiaryPost.builder()
|
||||
.id(snowflakeIdGenerator.nextIdAsString()) // 使用雪花算法生成ID
|
||||
.userId(request.getUserId())
|
||||
.title(request.getTitle())
|
||||
.content(request.getContent())
|
||||
.images(convertListToJson(request.getImages()))
|
||||
.videos(convertListToJson(request.getVideos()))
|
||||
.location(request.getLocation())
|
||||
.weather(request.getWeather())
|
||||
.mood(request.getMood())
|
||||
.tags(convertListToJson(request.getTags()))
|
||||
.isPublic(request.getIsPublic())
|
||||
.isAnonymous(request.getIsAnonymous())
|
||||
.viewCount(0)
|
||||
.likeCount(0)
|
||||
.commentCount(0)
|
||||
.shareCount(0)
|
||||
.publishTime(LocalDateTime.now())
|
||||
.status("published")
|
||||
.priority(0)
|
||||
.featured(0)
|
||||
.build();
|
||||
this.save(diaryPost);
|
||||
|
||||
// 2. 生成AI评论
|
||||
String aiComment = null;
|
||||
try {
|
||||
String conversationId = "diary-" + diaryPost.getId();
|
||||
aiComment = aiChatService.sendSummaryMessage(conversationId, diaryPost.getContent(), request.getUserId());
|
||||
} catch (Exception e) {
|
||||
aiComment = "开开:AI评论生成失败";
|
||||
}
|
||||
// 3. 写入AI评论到diary_comment表
|
||||
if (aiComment != null) {
|
||||
// 使用createCommentWithResponse方法创建AI评论
|
||||
com.emotion.dto.request.DiaryCommentCreateRequest commentRequest = new com.emotion.dto.request.DiaryCommentCreateRequest();
|
||||
commentRequest.setDiaryId(diaryPost.getId());
|
||||
commentRequest.setUserId("system"); // AI评论使用system用户ID
|
||||
commentRequest.setContent(aiComment);
|
||||
commentRequest.setIsAnonymous(0);
|
||||
diaryCommentService.createCommentWithResponse(commentRequest);
|
||||
|
||||
addAiComment(
|
||||
diaryPost.getId(),
|
||||
aiComment,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
// 4. 返回日记详情(含AI评论)
|
||||
return convertToResponse(diaryPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加AI评论
|
||||
*/
|
||||
private boolean addAiComment(String diaryId, String aiComment, Object aiEmotionAnalysis,
|
||||
BigDecimal aiSentimentScore, List<String> aiKeywords, String aiSuggestions) {
|
||||
LambdaUpdateWrapper<DiaryPost> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DiaryPost::getId, diaryId)
|
||||
.set(DiaryPost::getAiComment, aiComment)
|
||||
.set(DiaryPost::getAiCommentTime, LocalDateTime.now())
|
||||
.set(DiaryPost::getAiEmotionAnalysis, convertObjectToJson(aiEmotionAnalysis))
|
||||
.set(DiaryPost::getAiSentimentScore, aiSentimentScore)
|
||||
.set(DiaryPost::getAiKeywords, convertListToJson(aiKeywords))
|
||||
.set(DiaryPost::getAiSuggestions, aiSuggestions)
|
||||
.set(DiaryPost::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建PageResult对象
|
||||
*/
|
||||
private <T> PageResult<T> createPageResult(IPage<?> page, List<T> records) {
|
||||
PageResult<T> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(records);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将List转换为JSON字符串
|
||||
*/
|
||||
private String convertListToJson(List<?> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(list);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Object转换为JSON字符串
|
||||
*/
|
||||
private String convertObjectToJson(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换实体为响应DTO
|
||||
*/
|
||||
private DiaryPostResponse convertToResponse(DiaryPost diaryPost) {
|
||||
DiaryPostResponse response = new DiaryPostResponse();
|
||||
BeanUtils.copyProperties(diaryPost, response);
|
||||
|
||||
// 转换时间格式
|
||||
if (diaryPost.getPublishTime() != null) {
|
||||
response.setPublishTime(diaryPost.getPublishTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (diaryPost.getLastCommentTime() != null) {
|
||||
response.setLastCommentTime(diaryPost.getLastCommentTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (diaryPost.getAiCommentTime() != null) {
|
||||
response.setAiCommentTime(diaryPost.getAiCommentTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (diaryPost.getCreateTime() != null) {
|
||||
response.setCreateTime(diaryPost.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (diaryPost.getUpdateTime() != null) {
|
||||
response.setUpdateTime(diaryPost.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
|
||||
// 转换JSON字段
|
||||
try {
|
||||
if (diaryPost.getImages() != null) {
|
||||
response.setImages(objectMapper.readValue(diaryPost.getImages(), new TypeReference<List<String>>() {
|
||||
}));
|
||||
}
|
||||
if (diaryPost.getVideos() != null) {
|
||||
response.setVideos(objectMapper.readValue(diaryPost.getVideos(), new TypeReference<List<String>>() {
|
||||
}));
|
||||
}
|
||||
if (diaryPost.getTags() != null) {
|
||||
response.setTags(objectMapper.readValue(diaryPost.getTags(), new TypeReference<List<String>>() {
|
||||
}));
|
||||
}
|
||||
if (diaryPost.getAiKeywords() != null) {
|
||||
response.setAiKeywords(
|
||||
objectMapper.readValue(diaryPost.getAiKeywords(), new TypeReference<List<String>>() {
|
||||
}));
|
||||
}
|
||||
if (diaryPost.getAiEmotionAnalysis() != null) {
|
||||
response.setAiEmotionAnalysis(objectMapper.readValue(diaryPost.getAiEmotionAnalysis(), Object.class));
|
||||
}
|
||||
if (diaryPost.getMetadata() != null) {
|
||||
response.setMetadata(objectMapper.readValue(diaryPost.getMetadata(), Object.class));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
// 忽略JSON解析错误
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.entity.Dictionary;
|
||||
import com.emotion.mapper.DictionaryMapper;
|
||||
import com.emotion.service.DictionaryService;
|
||||
import com.emotion.dto.request.dictionary.DictionaryCreateRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryPageRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryUpdateRequest;
|
||||
import com.emotion.dto.response.dictionary.DictionaryResponse;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 字典Service实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@Service
|
||||
public class DictionaryServiceImpl extends ServiceImpl<DictionaryMapper, Dictionary> implements DictionaryService {
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 创建结果
|
||||
*/
|
||||
@Override
|
||||
public Result<DictionaryResponse> createDictionary(DictionaryCreateRequest request) {
|
||||
// 检查字典类型+字典编码是否已存在
|
||||
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Dictionary::getDictType, request.getDictType())
|
||||
.eq(Dictionary::getDictCode, request.getDictCode())
|
||||
.eq(Dictionary::getIsDeleted, 0);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
return Result.error(400, "同一字典类型下字典编码已存在");
|
||||
}
|
||||
|
||||
// 创建字典实体
|
||||
Dictionary dictionary = new Dictionary();
|
||||
BeanUtils.copyProperties(request, dictionary);
|
||||
dictionary.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0);
|
||||
|
||||
// 保存字典
|
||||
if (this.save(dictionary)) {
|
||||
DictionaryResponse response = convertToResponse(dictionary);
|
||||
return Result.success(response);
|
||||
}
|
||||
return Result.error(500, "创建字典失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 更新结果
|
||||
*/
|
||||
@Override
|
||||
public Result<DictionaryResponse> updateDictionary(DictionaryUpdateRequest request) {
|
||||
// 检查字典是否存在
|
||||
Dictionary dictionary = this.getById(request.getId());
|
||||
if (dictionary == null || dictionary.getIsDeleted() == 1) {
|
||||
return Result.error(400, "字典不存在");
|
||||
}
|
||||
|
||||
// 检查字典类型+字典编码是否已存在(排除当前字典)
|
||||
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Dictionary::getDictType, request.getDictType())
|
||||
.eq(Dictionary::getDictCode, request.getDictCode())
|
||||
.ne(Dictionary::getId, request.getId())
|
||||
.eq(Dictionary::getIsDeleted, 0);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
return Result.error(400, "同一字典类型下字典编码已存在");
|
||||
}
|
||||
|
||||
// 更新字典实体
|
||||
BeanUtils.copyProperties(request, dictionary);
|
||||
|
||||
// 保存更新
|
||||
if (this.updateById(dictionary)) {
|
||||
DictionaryResponse response = convertToResponse(dictionary);
|
||||
return Result.success(response);
|
||||
}
|
||||
return Result.error(500, "更新字典失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@Override
|
||||
public Result<Void> deleteDictionary(String id) {
|
||||
// 检查字典是否存在
|
||||
Dictionary dictionary = this.getById(id);
|
||||
if (dictionary == null || dictionary.getIsDeleted() == 1) {
|
||||
return Result.error(400, "字典不存在");
|
||||
}
|
||||
|
||||
// 删除字典(逻辑删除)
|
||||
if (this.removeById(id)) {
|
||||
return Result.success();
|
||||
}
|
||||
return Result.error(500, "删除字典失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 字典详情
|
||||
*/
|
||||
@Override
|
||||
public Result<DictionaryResponse> getDictionary(String id) {
|
||||
// 获取字典
|
||||
Dictionary dictionary = this.getById(id);
|
||||
if (dictionary == null || dictionary.getIsDeleted() == 1) {
|
||||
return Result.error(400, "字典不存在");
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
DictionaryResponse response = convertToResponse(dictionary);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询字典
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
@Override
|
||||
public Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request) {
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Dictionary::getIsDeleted, 0);
|
||||
|
||||
// 添加查询条件
|
||||
if (request.getDictType() != null && !request.getDictType().isEmpty()) {
|
||||
queryWrapper.eq(Dictionary::getDictType, request.getDictType());
|
||||
}
|
||||
if (request.getDictCode() != null && !request.getDictCode().isEmpty()) {
|
||||
queryWrapper.like(Dictionary::getDictCode, request.getDictCode());
|
||||
}
|
||||
if (request.getDictName() != null && !request.getDictName().isEmpty()) {
|
||||
queryWrapper.like(Dictionary::getDictName, request.getDictName());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
queryWrapper.eq(Dictionary::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
// 排序
|
||||
queryWrapper.orderByAsc(Dictionary::getDictType).orderByAsc(Dictionary::getSortOrder).orderByAsc(Dictionary::getCreateTime);
|
||||
|
||||
// 分页查询
|
||||
Page<Dictionary> page = this.page(new Page<>(request.getCurrent(), request.getSize()), queryWrapper);
|
||||
|
||||
// 转换为响应对象
|
||||
List<DictionaryResponse> responseList = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建分页结果
|
||||
PageResult<DictionaryResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responseList);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型查询字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 字典集合
|
||||
*/
|
||||
@Override
|
||||
public Result<List<DictionaryResponse>> getDictionariesByType(String dictType) {
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Dictionary::getDictType, dictType)
|
||||
.eq(Dictionary::getIsDeleted, 0)
|
||||
.orderByAsc(Dictionary::getSortOrder)
|
||||
.orderByAsc(Dictionary::getCreateTime);
|
||||
|
||||
// 查询字典集合
|
||||
List<Dictionary> dictionaryList = this.list(queryWrapper);
|
||||
|
||||
// 转换为响应对象
|
||||
List<DictionaryResponse> responseList = dictionaryList.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Result.success(responseList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型查询启用的字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 启用的字典集合
|
||||
*/
|
||||
@Override
|
||||
public Result<List<DictionaryResponse>> getEnabledDictionariesByType(String dictType) {
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Dictionary::getDictType, dictType)
|
||||
.eq(Dictionary::getStatus, 1)
|
||||
.eq(Dictionary::getIsDeleted, 0)
|
||||
.orderByAsc(Dictionary::getSortOrder)
|
||||
.orderByAsc(Dictionary::getCreateTime);
|
||||
|
||||
// 查询字典集合
|
||||
List<Dictionary> dictionaryList = this.list(queryWrapper);
|
||||
|
||||
// 转换为响应对象
|
||||
List<DictionaryResponse> responseList = dictionaryList.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Result.success(responseList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体转换为响应对象
|
||||
*
|
||||
* @param dictionary 字典实体
|
||||
* @return 字典响应对象
|
||||
*/
|
||||
private DictionaryResponse convertToResponse(Dictionary dictionary) {
|
||||
DictionaryResponse response = new DictionaryResponse();
|
||||
// 基础字段映射
|
||||
response.setId(dictionary.getId());
|
||||
response.setDictType(dictionary.getDictType());
|
||||
response.setDictCode(dictionary.getDictCode());
|
||||
response.setDictName(dictionary.getDictName());
|
||||
response.setDictValue(dictionary.getDictValue());
|
||||
response.setSortOrder(dictionary.getSortOrder());
|
||||
response.setStatus(dictionary.getStatus());
|
||||
response.setCreateBy(dictionary.getCreateBy());
|
||||
response.setUpdateBy(dictionary.getUpdateBy());
|
||||
response.setRemarks(dictionary.getRemarks());
|
||||
|
||||
// 转换时间类型
|
||||
if (dictionary.getCreateTime() != null) {
|
||||
response.setCreateTime(dictionary.getCreateTime().toString());
|
||||
}
|
||||
if (dictionary.getUpdateTime() != null) {
|
||||
response.setUpdateTime(dictionary.getUpdateTime().toString());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.EmotionAnalysisCreateRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisPageRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionAnalysisResponse;
|
||||
import com.emotion.entity.EmotionAnalysis;
|
||||
import com.emotion.mapper.EmotionAnalysisMapper;
|
||||
import com.emotion.service.EmotionAnalysisService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 情绪分析服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class EmotionAnalysisServiceImpl extends ServiceImpl<EmotionAnalysisMapper, EmotionAnalysis> implements EmotionAnalysisService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<EmotionAnalysis> getPage(BasePageRequest request) {
|
||||
Page<EmotionAnalysis> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(EmotionAnalysis::getPrimaryEmotion, request.getKeyword())
|
||||
.or().like(EmotionAnalysis::getPolarity, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(EmotionAnalysis::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(EmotionAnalysis::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(EmotionAnalysis::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(EmotionAnalysis::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<EmotionAnalysis> getPageByUserId(BasePageRequest request, String userId) {
|
||||
Page<EmotionAnalysis> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EmotionAnalysis::getCreateBy, userId)
|
||||
.eq(EmotionAnalysis::getIsDeleted, 0)
|
||||
.orderByDesc(EmotionAnalysis::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysis getByMessageId(String messageId) {
|
||||
LambdaQueryWrapper<EmotionAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EmotionAnalysis::getMessageId, messageId)
|
||||
.eq(EmotionAnalysis::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysis createEmotionAnalysis(String messageId, String userId, String primaryEmotion,
|
||||
String polarity, Double intensity, Double confidence) {
|
||||
EmotionAnalysis analysis = new EmotionAnalysis();
|
||||
analysis.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
analysis.setMessageId(messageId);
|
||||
analysis.setCreateBy(userId);
|
||||
analysis.setPrimaryEmotion(primaryEmotion);
|
||||
analysis.setPolarity(polarity);
|
||||
analysis.setIntensity(BigDecimal.valueOf(intensity));
|
||||
analysis.setConfidence(BigDecimal.valueOf(confidence));
|
||||
|
||||
this.save(analysis);
|
||||
return analysis;
|
||||
}
|
||||
|
||||
// 新增的方法实现
|
||||
|
||||
@Override
|
||||
public PageResult<EmotionAnalysisResponse> getPageWithResponse(EmotionAnalysisPageRequest request) {
|
||||
IPage<EmotionAnalysis> page = getPage(request);
|
||||
List<EmotionAnalysisResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
return createPageResult(page, responses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<EmotionAnalysisResponse> getPageByUserIdWithResponse(String userId,
|
||||
EmotionAnalysisPageRequest request) {
|
||||
IPage<EmotionAnalysis> page = getPageByUserId(request, userId);
|
||||
List<EmotionAnalysisResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
return createPageResult(page, responses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysisResponse getEmotionAnalysisResponseById(String id) {
|
||||
EmotionAnalysis analysis = this.getById(id);
|
||||
if (analysis == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysisResponse getEmotionAnalysisResponseByMessageId(String messageId) {
|
||||
EmotionAnalysis analysis = getByMessageId(messageId);
|
||||
if (analysis == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysisResponse createEmotionAnalysisWithResponse(EmotionAnalysisCreateRequest request) {
|
||||
EmotionAnalysis analysis = createEmotionAnalysis(
|
||||
request.getMessageId(),
|
||||
request.getUserId(),
|
||||
request.getPrimaryEmotion(),
|
||||
request.getPolarity(),
|
||||
request.getIntensity(),
|
||||
request.getConfidence());
|
||||
return convertToResponse(analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysisResponse updateEmotionAnalysisWithResponse(EmotionAnalysisUpdateRequest request) {
|
||||
EmotionAnalysis analysis = this.getById(request.getId());
|
||||
if (analysis == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (request.getMessageId() != null) {
|
||||
analysis.setMessageId(request.getMessageId());
|
||||
}
|
||||
if (request.getUserId() != null) {
|
||||
analysis.setCreateBy(request.getUserId());
|
||||
}
|
||||
if (request.getPrimaryEmotion() != null) {
|
||||
analysis.setPrimaryEmotion(request.getPrimaryEmotion());
|
||||
}
|
||||
if (request.getPolarity() != null) {
|
||||
analysis.setPolarity(request.getPolarity());
|
||||
}
|
||||
if (request.getIntensity() != null) {
|
||||
analysis.setIntensity(BigDecimal.valueOf(request.getIntensity()));
|
||||
}
|
||||
if (request.getConfidence() != null) {
|
||||
analysis.setConfidence(BigDecimal.valueOf(request.getConfidence()));
|
||||
}
|
||||
|
||||
this.updateById(analysis);
|
||||
return convertToResponse(analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteEmotionAnalysis(String id) {
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建PageResult对象
|
||||
*/
|
||||
private <T> PageResult<T> createPageResult(IPage<?> page, List<T> records) {
|
||||
PageResult<T> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(records);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private EmotionAnalysisResponse convertToResponse(EmotionAnalysis analysis) {
|
||||
EmotionAnalysisResponse response = new EmotionAnalysisResponse();
|
||||
BeanUtils.copyProperties(analysis, response);
|
||||
response.setId(analysis.getId());
|
||||
if (analysis.getCreateTime() != null) {
|
||||
response.setCreateTime(analysis.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (analysis.getUpdateTime() != null) {
|
||||
response.setUpdateTime(analysis.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
// 处理Double类型转换
|
||||
if (analysis.getIntensity() != null) {
|
||||
response.setIntensity(analysis.getIntensity().doubleValue());
|
||||
}
|
||||
if (analysis.getConfidence() != null) {
|
||||
response.setConfidence(analysis.getConfidence().doubleValue());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.EmotionRecordCreateRequest;
|
||||
import com.emotion.dto.request.EmotionRecordPageRequest;
|
||||
import com.emotion.dto.request.EmotionRecordUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionRecordResponse;
|
||||
import com.emotion.entity.EmotionRecord;
|
||||
import com.emotion.mapper.EmotionRecordMapper;
|
||||
import com.emotion.service.EmotionRecordService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 情绪记录服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class EmotionRecordServiceImpl extends ServiceImpl<EmotionRecordMapper, EmotionRecord> implements EmotionRecordService {
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<EmotionRecord> getPage(BasePageRequest request) {
|
||||
Page<EmotionRecord> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(EmotionRecord::getDescription, request.getKeyword())
|
||||
.or().like(EmotionRecord::getNotes, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(EmotionRecord::getIsDeleted, 0).orderByDesc(EmotionRecord::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId) {
|
||||
Page<EmotionRecord> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EmotionRecord::getUserId, userId)
|
||||
.eq(EmotionRecord::getIsDeleted, 0)
|
||||
.orderByDesc(EmotionRecord::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionRecord createEmotionRecord(String userId, String emotionType, Double intensity,
|
||||
String trigger, String location, String notes) {
|
||||
EmotionRecord record = new EmotionRecord();
|
||||
record.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
record.setUserId(userId);
|
||||
record.setEmotionType(emotionType);
|
||||
record.setIntensity(BigDecimal.valueOf(intensity));
|
||||
record.setTriggers(trigger);
|
||||
record.setLocation(location);
|
||||
record.setNotes(notes);
|
||||
record.setRecordDate(LocalDate.now());
|
||||
|
||||
this.save(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
// 新增的方法实现
|
||||
|
||||
@Override
|
||||
public PageResult<EmotionRecordResponse> getPageWithResponse(EmotionRecordPageRequest request) {
|
||||
IPage<EmotionRecord> page = getPage(request);
|
||||
List<EmotionRecordResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<EmotionRecordResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<EmotionRecordResponse> getPageByUserIdWithResponse(String userId, EmotionRecordPageRequest request) {
|
||||
IPage<EmotionRecord> page = getPageByUserId(request, userId);
|
||||
List<EmotionRecordResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<EmotionRecordResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionRecordResponse getEmotionRecordResponseById(String id) {
|
||||
EmotionRecord record = this.getById(id);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionRecordResponse createEmotionRecordWithResponse(EmotionRecordCreateRequest request) {
|
||||
EmotionRecord record = new EmotionRecord();
|
||||
record.setId(snowflakeIdGenerator.nextIdAsString()); // 使用雪花算法生成ID
|
||||
record.setUserId(request.getUserId());
|
||||
record.setRecordDate(request.getRecordDate());
|
||||
record.setEmotionType(request.getEmotionType());
|
||||
record.setIntensity(request.getIntensity());
|
||||
record.setTriggers(request.getTriggers());
|
||||
record.setDescription(request.getDescription());
|
||||
// 注意:tags字段在实体类中是String类型,需要转换
|
||||
record.setTags(request.getTags() != null ? request.getTags().toString() : null);
|
||||
record.setWeather(request.getWeather());
|
||||
record.setLocation(request.getLocation());
|
||||
record.setActivity(request.getActivity());
|
||||
record.setPeople(request.getPeople());
|
||||
record.setNotes(request.getNotes());
|
||||
|
||||
this.save(record);
|
||||
return convertToResponse(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionRecordResponse updateEmotionRecordWithResponse(EmotionRecordUpdateRequest request) {
|
||||
EmotionRecord record = this.getById(request.getId());
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (request.getRecordDate() != null) {
|
||||
record.setRecordDate(request.getRecordDate());
|
||||
}
|
||||
if (request.getEmotionType() != null) {
|
||||
record.setEmotionType(request.getEmotionType());
|
||||
}
|
||||
if (request.getIntensity() != null) {
|
||||
record.setIntensity(request.getIntensity());
|
||||
}
|
||||
if (request.getTriggers() != null) {
|
||||
record.setTriggers(request.getTriggers());
|
||||
}
|
||||
if (request.getDescription() != null) {
|
||||
record.setDescription(request.getDescription());
|
||||
}
|
||||
// 注意:tags字段在实体类中是String类型,需要转换
|
||||
if (request.getTags() != null) {
|
||||
record.setTags(request.getTags().toString());
|
||||
}
|
||||
if (request.getWeather() != null) {
|
||||
record.setWeather(request.getWeather());
|
||||
}
|
||||
if (request.getLocation() != null) {
|
||||
record.setLocation(request.getLocation());
|
||||
}
|
||||
if (request.getActivity() != null) {
|
||||
record.setActivity(request.getActivity());
|
||||
}
|
||||
if (request.getPeople() != null) {
|
||||
record.setPeople(request.getPeople());
|
||||
}
|
||||
if (request.getNotes() != null) {
|
||||
record.setNotes(request.getNotes());
|
||||
}
|
||||
|
||||
this.updateById(record);
|
||||
return convertToResponse(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteEmotionRecord(String id) {
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getEmotionStats(String userId, String startDate, String endDate) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EmotionRecord::getUserId, userId)
|
||||
.eq(EmotionRecord::getIsDeleted, 0);
|
||||
|
||||
// 如果提供了日期范围,则添加时间条件
|
||||
if (StringUtils.hasText(startDate) && StringUtils.hasText(endDate)) {
|
||||
try {
|
||||
LocalDateTime start = LocalDate.parse(startDate).atStartOfDay();
|
||||
LocalDateTime end = LocalDate.parse(endDate).atTime(23, 59, 59);
|
||||
wrapper.between(EmotionRecord::getCreateTime, start, end);
|
||||
} catch (Exception e) {
|
||||
// 如果日期解析失败,忽略日期条件
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户的所有情绪记录
|
||||
List<EmotionRecord> records = this.list(wrapper);
|
||||
|
||||
if (records.isEmpty()) {
|
||||
stats.put("emotionDistribution", new HashMap<>());
|
||||
stats.put("totalRecords", 0);
|
||||
stats.put("averageIntensity", 0.0);
|
||||
stats.put("mostFrequentEmotion", null);
|
||||
stats.put("emotionTrend", "no_data");
|
||||
return stats;
|
||||
}
|
||||
|
||||
// 情绪类型分布
|
||||
Map<String, Long> emotionDistribution = records.stream()
|
||||
.collect(Collectors.groupingBy(EmotionRecord::getEmotionType, Collectors.counting()));
|
||||
|
||||
stats.put("emotionDistribution", emotionDistribution);
|
||||
stats.put("totalRecords", records.size());
|
||||
|
||||
// 平均情绪强度
|
||||
double avgIntensity = records.stream()
|
||||
.mapToDouble(record -> record.getIntensity().doubleValue())
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
stats.put("averageIntensity", avgIntensity);
|
||||
|
||||
// 最常见的情绪类型
|
||||
String mostFrequentEmotion = emotionDistribution.entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse(null);
|
||||
stats.put("mostFrequentEmotion", mostFrequentEmotion);
|
||||
|
||||
// 情绪趋势(简单实现:比较前半段和后半段的平均强度)
|
||||
if (records.size() > 1) {
|
||||
int mid = records.size() / 2;
|
||||
double firstHalfAvg = records.subList(0, mid).stream()
|
||||
.mapToDouble(record -> record.getIntensity().doubleValue())
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
double secondHalfAvg = records.subList(mid, records.size()).stream()
|
||||
.mapToDouble(record -> record.getIntensity().doubleValue())
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
if (secondHalfAvg > firstHalfAvg) {
|
||||
stats.put("emotionTrend", "improving");
|
||||
} else if (secondHalfAvg < firstHalfAvg) {
|
||||
stats.put("emotionTrend", "declining");
|
||||
} else {
|
||||
stats.put("emotionTrend", "stable");
|
||||
}
|
||||
} else {
|
||||
stats.put("emotionTrend", "insufficient_data");
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private EmotionRecordResponse convertToResponse(EmotionRecord record) {
|
||||
EmotionRecordResponse response = new EmotionRecordResponse();
|
||||
BeanUtils.copyProperties(record, response);
|
||||
response.setId(record.getId());
|
||||
if (record.getCreateTime() != null) {
|
||||
response.setCreateTime(record.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (record.getUpdateTime() != null) {
|
||||
response.setUpdateTime(record.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,714 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.request.EpicScriptCreateRequest;
|
||||
import com.emotion.dto.request.EpicScriptInspirationRequest;
|
||||
import com.emotion.dto.request.EpicScriptPageRequest;
|
||||
import com.emotion.dto.request.EpicScriptUpdateRequest;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.EpicScriptInspirationResponse;
|
||||
import com.emotion.dto.response.EpicScriptResponse;
|
||||
import com.emotion.dto.response.InspirationSuggestionResponse;
|
||||
import com.emotion.entity.EpicScript;
|
||||
import com.emotion.mapper.EpicScriptMapper;
|
||||
import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.EpicScriptService;
|
||||
import com.emotion.service.LifePathService;
|
||||
import com.emotion.service.ScriptContextService;
|
||||
import com.emotion.service.TtsTaskService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 爽文剧本服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScript>
|
||||
implements EpicScriptService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final int DAILY_INSPIRATION_LIMIT = 3;
|
||||
private static final List<InspirationSuggestionResponse> INSPIRATION_SUGGESTIONS = List.of(
|
||||
new InspirationSuggestionResponse("我想把最近一次低谷,改写成主角觉醒的开端。", "觉醒", "转折"),
|
||||
new InspirationSuggestionResponse("如果我在最遗憾的选择里勇敢了一次,人生会怎样展开?", "遗憾", "重启"),
|
||||
new InspirationSuggestionResponse("把一次普通的职场挑战,写成逆风翻盘的高光篇章。", "职场", "成长"),
|
||||
new InspirationSuggestionResponse("我想见到十年后的自己,让 TA 给现在的我一封信。", "未来", "对话"),
|
||||
new InspirationSuggestionResponse("把一段关系里的告别,写成重新认识自己的旅程。", "关系", "治愈"),
|
||||
new InspirationSuggestionResponse("让我的童年记忆成为故事里的隐藏力量。", "童年", "力量"),
|
||||
new InspirationSuggestionResponse("把一次失败的面试、考试或竞赛,改写成命运伏笔。", "挑战", "伏笔"),
|
||||
new InspirationSuggestionResponse("写一个我终于不再讨好别人,开始选择自己的平行人生。", "自我", "选择")
|
||||
);
|
||||
|
||||
/**
|
||||
* Coze工作流配置键 - 爽文剧本生成
|
||||
*/
|
||||
@Autowired
|
||||
@Lazy
|
||||
private LifePathService lifePathService;
|
||||
|
||||
@Autowired
|
||||
private AiRuntimeService aiRuntimeService;
|
||||
|
||||
@Autowired
|
||||
private ScriptContextService scriptContextService;
|
||||
|
||||
@Autowired
|
||||
private TtsTaskService ttsTaskService;
|
||||
|
||||
@Override
|
||||
public PageResult<EpicScriptResponse> getPageByCurrentUser(EpicScriptPageRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return new PageResult<>();
|
||||
}
|
||||
|
||||
Page<EpicScript> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EpicScript::getUserId, currentUserId)
|
||||
.eq(EpicScript::getIsDeleted, 0);
|
||||
|
||||
// 风格筛选
|
||||
if (StringUtils.hasText(request.getStyle())) {
|
||||
wrapper.eq(EpicScript::getStyle, request.getStyle());
|
||||
}
|
||||
|
||||
// 篇幅筛选
|
||||
if (StringUtils.hasText(request.getLength())) {
|
||||
wrapper.eq(EpicScript::getLength, request.getLength());
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(EpicScript::getTitle, request.getKeyword())
|
||||
.or().like(EpicScript::getTheme, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 按创建时间倒序排列
|
||||
wrapper.orderByDesc(EpicScript::getCreateTime);
|
||||
|
||||
Page<EpicScript> resultPage = this.page(page, wrapper);
|
||||
List<EpicScriptResponse> responses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<EpicScriptResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EpicScriptResponse> getListByCurrentUser() {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EpicScript::getUserId, currentUserId)
|
||||
.eq(EpicScript::getIsDeleted, 0)
|
||||
.orderByDesc(EpicScript::getCreateTime);
|
||||
|
||||
return this.list(wrapper).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpicScriptResponse getScriptById(String id) {
|
||||
EpicScript script = this.getById(id);
|
||||
if (script == null || script.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!script.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpicScriptResponse createScript(EpicScriptCreateRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EpicScript script = new EpicScript();
|
||||
script.setUserId(currentUserId);
|
||||
script.setTitle(request.getTitle());
|
||||
script.setTheme(request.getTheme());
|
||||
script.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
|
||||
script.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
|
||||
script.setPlotIntro(request.getPlotIntro());
|
||||
script.setPlotTurning(request.getPlotTurning());
|
||||
script.setPlotClimax(request.getPlotClimax());
|
||||
script.setPlotEnding(request.getPlotEnding());
|
||||
script.setPlotJson(request.getPlotJson());
|
||||
script.setIsSelected(request.getIsSelected() != null && request.getIsSelected() ? 1 : 0);
|
||||
|
||||
// 调用Coze AI生成剧本内容
|
||||
String existingContent = extractExistingGeneratedContent(script.getPlotJson());
|
||||
String aiGeneratedContent = StringUtils.hasText(existingContent)
|
||||
? existingContent
|
||||
: generateScriptByAi(request, currentUserId);
|
||||
if (aiGeneratedContent != null) {
|
||||
// 将AI生成的内容存储到plotJson中
|
||||
Map<String, Object> plotJson = script.getPlotJson();
|
||||
if (plotJson == null) {
|
||||
plotJson = new java.util.HashMap<>();
|
||||
}
|
||||
plotJson.put("fullContent", aiGeneratedContent);
|
||||
List<String> socialInsightLabels = scriptContextService.getConfirmedInsightLabels(currentUserId);
|
||||
if (!Boolean.FALSE.equals(request.getUseSocialInsights()) && !socialInsightLabels.isEmpty()) {
|
||||
plotJson.put("socialInsightLabels", socialInsightLabels);
|
||||
}
|
||||
script.setPlotJson(plotJson);
|
||||
log.info("AI生成剧本内容成功,用户ID: {}, 内容长度: {}", currentUserId, aiGeneratedContent.length());
|
||||
}
|
||||
|
||||
this.save(script);
|
||||
prewarmScriptTts(script);
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InspirationSuggestionResponse> getInspirationRecommendations() {
|
||||
return INSPIRATION_SUGGESTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InspirationSuggestionResponse> getRandomInspirations(Integer size) {
|
||||
int limit = size == null ? 3 : Math.max(1, Math.min(size, INSPIRATION_SUGGESTIONS.size()));
|
||||
List<InspirationSuggestionResponse> suggestions = new ArrayList<>(INSPIRATION_SUGGESTIONS);
|
||||
Collections.shuffle(suggestions);
|
||||
return suggestions.subList(0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpicScriptInspirationResponse generateFromInspiration(EpicScriptInspirationRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int usedToday = countTodayScripts(currentUserId);
|
||||
if (usedToday >= DAILY_INSPIRATION_LIMIT) {
|
||||
throw new IllegalStateException("今日灵感生成次数已用完");
|
||||
}
|
||||
|
||||
String prompt = request.getPrompt().trim();
|
||||
EpicScriptCreateRequest createRequest = new EpicScriptCreateRequest();
|
||||
createRequest.setTitle(buildInspirationTitle(prompt));
|
||||
createRequest.setTheme(prompt);
|
||||
createRequest.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career");
|
||||
createRequest.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium");
|
||||
createRequest.setCharacterInfo(request.getCharacterInfo());
|
||||
createRequest.setLifeEventsSummary(request.getLifeEventsSummary());
|
||||
createRequest.setUseSocialInsights(request.getUseSocialInsights());
|
||||
|
||||
Map<String, Object> plotJson = new HashMap<>();
|
||||
plotJson.put("mode", "inspiration");
|
||||
plotJson.put("prompt", prompt);
|
||||
plotJson.put("source", StringUtils.hasText(request.getSource()) ? request.getSource() : "mini-program");
|
||||
createRequest.setPlotJson(plotJson);
|
||||
|
||||
EpicScriptResponse script = createScript(createRequest);
|
||||
EpicScriptInspirationResponse response = new EpicScriptInspirationResponse();
|
||||
response.setScript(script);
|
||||
response.setPrompt(prompt);
|
||||
response.setRemainingCount(Math.max(0, DAILY_INSPIRATION_LIMIT - usedToday - 1));
|
||||
response.setSuggestions(getRandomInspirations(3));
|
||||
return response;
|
||||
}
|
||||
|
||||
private int countTodayScripts(String userId) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime start = today.atStartOfDay();
|
||||
LocalDateTime end = today.plusDays(1).atStartOfDay();
|
||||
|
||||
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EpicScript::getUserId, userId)
|
||||
.eq(EpicScript::getIsDeleted, 0)
|
||||
.ge(EpicScript::getCreateTime, start)
|
||||
.lt(EpicScript::getCreateTime, end);
|
||||
return Math.toIntExact(this.count(wrapper));
|
||||
}
|
||||
|
||||
private String buildInspirationTitle(String prompt) {
|
||||
String normalized = prompt.replaceAll("\\s+", " ").trim();
|
||||
if (normalized.length() <= 22) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, 22) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Coze AI生成爽文剧本内容
|
||||
*
|
||||
* @param request 剧本创建请求
|
||||
* @param userId 用户ID
|
||||
* @return AI生成的剧本内容,失败时返回null
|
||||
*/
|
||||
private String generateScriptByAi(EpicScriptCreateRequest request, String userId) {
|
||||
try {
|
||||
// 组装AI输入
|
||||
String input = assembleScriptInput(request, userId);
|
||||
log.info("开始调用AI生成剧本,用户ID: {}, 输入长度: {}", userId, input.length());
|
||||
|
||||
String result = invokeScriptRuntime(request, input, userId);
|
||||
|
||||
log.info("AI生成剧本完成,用户ID: {}, 结果长度: {}", userId, result != null ? result.length() : 0);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI生成剧本失败,用户ID: {}, 错误: {}", userId, e.getMessage(), e);
|
||||
// AI调用失败不影响剧本创建,返回null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String invokeScriptRuntime(EpicScriptCreateRequest request, String input, String userId) {
|
||||
JSONObject inputs = new JSONObject();
|
||||
inputs.put("input", input);
|
||||
inputs.put("prompt", StringUtils.hasText(request.getTheme()) ? request.getTheme() : input);
|
||||
inputs.put("theme", request.getTheme());
|
||||
inputs.put("style", request.getStyle());
|
||||
inputs.put("length", request.getLength());
|
||||
inputs.put("useSocialInsights", request.getUseSocialInsights());
|
||||
return invokeRuntime("script_generate", inputs, userId);
|
||||
}
|
||||
|
||||
private String invokeScriptRuntime(EpicScriptUpdateRequest request, String input, String userId) {
|
||||
JSONObject inputs = new JSONObject();
|
||||
inputs.put("input", input);
|
||||
inputs.put("prompt", StringUtils.hasText(request.getTheme()) ? request.getTheme() : input);
|
||||
inputs.put("theme", request.getTheme());
|
||||
inputs.put("style", request.getStyle());
|
||||
inputs.put("length", request.getLength());
|
||||
inputs.put("useSocialInsights", request.getUseSocialInsights());
|
||||
return invokeRuntime("script_generate", inputs, userId);
|
||||
}
|
||||
|
||||
private String invokeRuntime(String sceneCode, JSONObject inputs, String userId) {
|
||||
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
|
||||
runtimeRequest.setSceneCode(sceneCode);
|
||||
runtimeRequest.setUserId(userId);
|
||||
runtimeRequest.setInputs(inputs);
|
||||
AiRuntimeTestResponse response = aiRuntimeService.test(runtimeRequest);
|
||||
if (response == null || !"success".equals(response.getStatus()) || !StringUtils.hasText(response.getOutput())) {
|
||||
String message = response == null ? "AI_RUNTIME_EMPTY_RESPONSE" : response.getErrorMessage();
|
||||
throw new IllegalStateException(StringUtils.hasText(message) ? message : "AI_RUNTIME_EMPTY_RESPONSE");
|
||||
}
|
||||
return response.getOutput();
|
||||
}
|
||||
|
||||
private String extractExistingGeneratedContent(Map<String, Object> plotJson) {
|
||||
if (plotJson == null) {
|
||||
return null;
|
||||
}
|
||||
Object fullContent = plotJson.get("fullContent");
|
||||
return fullContent == null ? null : String.valueOf(fullContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装AI输入内容
|
||||
* 将EpicScriptCreateRequest的字段组装为格式化字符串
|
||||
*
|
||||
* @param request 剧本创建请求
|
||||
* @return 格式化的输入字符串
|
||||
*/
|
||||
private String assembleScriptInput(EpicScriptCreateRequest request, String userId) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 角色信息
|
||||
if (StringUtils.hasText(request.getCharacterInfo())) {
|
||||
sb.append("【角色信息】").append(request.getCharacterInfo()).append("\n");
|
||||
}
|
||||
|
||||
// 过往经历
|
||||
if (StringUtils.hasText(request.getLifeEventsSummary())) {
|
||||
sb.append("【过往经历】").append(request.getLifeEventsSummary()).append("\n");
|
||||
}
|
||||
|
||||
String socialContext = scriptContextService == null
|
||||
? null
|
||||
: scriptContextService.buildSocialInsightContext(userId, request.getUseSocialInsights());
|
||||
if (StringUtils.hasText(socialContext)) {
|
||||
sb.append(socialContext).append("\n");
|
||||
}
|
||||
|
||||
// 标题
|
||||
if (StringUtils.hasText(request.getTitle())) {
|
||||
sb.append("【剧本标题】").append(request.getTitle()).append("\n");
|
||||
}
|
||||
|
||||
// 主题/渴望
|
||||
if (StringUtils.hasText(request.getTheme())) {
|
||||
sb.append("【主题渴望】").append(request.getTheme()).append("\n");
|
||||
}
|
||||
|
||||
// 风格
|
||||
if (StringUtils.hasText(request.getStyle())) {
|
||||
String styleDesc = getStyleDescription(request.getStyle());
|
||||
sb.append("【剧本风格】").append(styleDesc).append("\n");
|
||||
}
|
||||
|
||||
// 篇幅
|
||||
if (StringUtils.hasText(request.getLength())) {
|
||||
String lengthDesc = getLengthDescription(request.getLength());
|
||||
sb.append("【篇幅长度】").append(lengthDesc).append("\n");
|
||||
}
|
||||
|
||||
// 序幕:低谷回响
|
||||
if (StringUtils.hasText(request.getPlotIntro())) {
|
||||
sb.append("【序幕-低谷回响】").append(request.getPlotIntro()).append("\n");
|
||||
}
|
||||
|
||||
// 转折:契机出现
|
||||
if (StringUtils.hasText(request.getPlotTurning())) {
|
||||
sb.append("【转折-契机出现】").append(request.getPlotTurning()).append("\n");
|
||||
}
|
||||
|
||||
// 高潮:命运抉择
|
||||
if (StringUtils.hasText(request.getPlotClimax())) {
|
||||
sb.append("【高潮-命运抉择】").append(request.getPlotClimax()).append("\n");
|
||||
}
|
||||
|
||||
// 结局:新的开始
|
||||
if (StringUtils.hasText(request.getPlotEnding())) {
|
||||
sb.append("【结局-新的开始】").append(request.getPlotEnding()).append("\n");
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private String assembleScriptInput(EpicScriptCreateRequest request) {
|
||||
return assembleScriptInput(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取风格描述
|
||||
*
|
||||
* @param style 风格代码
|
||||
* @return 风格描述
|
||||
*/
|
||||
private String getStyleDescription(String style) {
|
||||
switch (style) {
|
||||
case "career":
|
||||
return "职场逆袭";
|
||||
case "love":
|
||||
return "情感圆满";
|
||||
case "fantasy":
|
||||
return "玄幻觉醒";
|
||||
default:
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取篇幅描述
|
||||
*
|
||||
* @param length 篇幅代码
|
||||
* @return 篇幅描述
|
||||
*/
|
||||
private String getLengthDescription(String length) {
|
||||
switch (length) {
|
||||
case "medium":
|
||||
return "标准篇";
|
||||
case "long":
|
||||
return "长篇";
|
||||
default:
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpicScriptResponse updateScript(EpicScriptUpdateRequest request) {
|
||||
EpicScript script = this.getById(request.getId());
|
||||
if (script == null || script.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!script.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (StringUtils.hasText(request.getTitle())) {
|
||||
script.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getTheme() != null) {
|
||||
script.setTheme(request.getTheme());
|
||||
}
|
||||
if (StringUtils.hasText(request.getStyle())) {
|
||||
script.setStyle(request.getStyle());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLength())) {
|
||||
script.setLength(request.getLength());
|
||||
}
|
||||
if (request.getPlotIntro() != null) {
|
||||
script.setPlotIntro(request.getPlotIntro());
|
||||
}
|
||||
if (request.getPlotTurning() != null) {
|
||||
script.setPlotTurning(request.getPlotTurning());
|
||||
}
|
||||
if (request.getPlotClimax() != null) {
|
||||
script.setPlotClimax(request.getPlotClimax());
|
||||
}
|
||||
if (request.getPlotEnding() != null) {
|
||||
script.setPlotEnding(request.getPlotEnding());
|
||||
}
|
||||
if (request.getPlotJson() != null) {
|
||||
script.setPlotJson(request.getPlotJson());
|
||||
}
|
||||
if (request.getIsSelected() != null) {
|
||||
script.setIsSelected(request.getIsSelected() ? 1 : 0);
|
||||
}
|
||||
|
||||
// 如果需要重新生成AI内容
|
||||
if (Boolean.TRUE.equals(request.getRegenerateContent())) {
|
||||
String aiGeneratedContent = regenerateScriptByAi(request, script, currentUserId);
|
||||
if (aiGeneratedContent != null) {
|
||||
Map<String, Object> plotJson = script.getPlotJson();
|
||||
if (plotJson == null) {
|
||||
plotJson = new java.util.HashMap<>();
|
||||
}
|
||||
plotJson.put("fullContent", aiGeneratedContent);
|
||||
List<String> socialInsightLabels = scriptContextService.getConfirmedInsightLabels(currentUserId);
|
||||
if (!Boolean.FALSE.equals(request.getUseSocialInsights()) && !socialInsightLabels.isEmpty()) {
|
||||
plotJson.put("socialInsightLabels", socialInsightLabels);
|
||||
}
|
||||
script.setPlotJson(plotJson);
|
||||
log.info("AI重新生成剧本内容成功,用户ID: {}, 剧本ID: {}", currentUserId, script.getId());
|
||||
}
|
||||
}
|
||||
|
||||
this.updateById(script);
|
||||
prewarmScriptTts(script);
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
private void prewarmScriptTts(EpicScript script) {
|
||||
if (script == null || !StringUtils.hasText(script.getId()) || !StringUtils.hasText(script.getUserId())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ttsTaskService.prewarmEpicScript(script.getUserId(), script.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Epic script TTS prewarm failed, scriptId={}, userId={}, error={}",
|
||||
script.getId(), script.getUserId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Coze AI重新生成爽文剧本内容
|
||||
*
|
||||
* @param request 剧本更新请求
|
||||
* @param script 原剧本实体
|
||||
* @param userId 用户ID
|
||||
* @return AI生成的剧本内容,失败时返回null
|
||||
*/
|
||||
private String regenerateScriptByAi(EpicScriptUpdateRequest request, EpicScript script, String userId) {
|
||||
try {
|
||||
// 组装AI输入
|
||||
String input = assembleUpdateScriptInput(request, script, userId);
|
||||
log.info("开始调用AI重新生成剧本,用户ID: {}, 剧本ID: {}", userId, script.getId());
|
||||
|
||||
String result = invokeScriptRuntime(request, input, userId);
|
||||
|
||||
log.info("AI重新生成剧本完成,用户ID: {}, 结果长度: {}", userId, result != null ? result.length() : 0);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI重新生成剧本失败,用户ID: {}, 剧本ID: {}, 错误: {}", userId, script.getId(), e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装更新时的AI输入内容
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @param script 原剧本实体
|
||||
* @return 格式化的输入字符串
|
||||
*/
|
||||
private String assembleUpdateScriptInput(EpicScriptUpdateRequest request, EpicScript script, String userId) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 角色信息(优先使用请求中的,否则使用原有的)
|
||||
String characterInfo = StringUtils.hasText(request.getCharacterInfo())
|
||||
? request.getCharacterInfo() : null;
|
||||
if (characterInfo != null) {
|
||||
sb.append("【角色信息】").append(characterInfo).append("\n");
|
||||
}
|
||||
|
||||
// 过往经历
|
||||
String lifeEventsSummary = StringUtils.hasText(request.getLifeEventsSummary())
|
||||
? request.getLifeEventsSummary() : null;
|
||||
if (lifeEventsSummary != null) {
|
||||
sb.append("【过往经历】").append(lifeEventsSummary).append("\n");
|
||||
}
|
||||
|
||||
String socialContext = scriptContextService == null
|
||||
? null
|
||||
: scriptContextService.buildSocialInsightContext(userId, request.getUseSocialInsights());
|
||||
if (StringUtils.hasText(socialContext)) {
|
||||
sb.append(socialContext).append("\n");
|
||||
}
|
||||
|
||||
// 标题
|
||||
String title = StringUtils.hasText(request.getTitle()) ? request.getTitle() : script.getTitle();
|
||||
if (StringUtils.hasText(title)) {
|
||||
sb.append("【剧本标题】").append(title).append("\n");
|
||||
}
|
||||
|
||||
// 主题/渴望
|
||||
String theme = request.getTheme() != null ? request.getTheme() : script.getTheme();
|
||||
if (StringUtils.hasText(theme)) {
|
||||
sb.append("【主题渴望】").append(theme).append("\n");
|
||||
}
|
||||
|
||||
// 风格
|
||||
String style = StringUtils.hasText(request.getStyle()) ? request.getStyle() : script.getStyle();
|
||||
if (StringUtils.hasText(style)) {
|
||||
String styleDesc = getStyleDescription(style);
|
||||
sb.append("【剧本风格】").append(styleDesc).append("\n");
|
||||
}
|
||||
|
||||
// 篇幅
|
||||
String length = StringUtils.hasText(request.getLength()) ? request.getLength() : script.getLength();
|
||||
if (StringUtils.hasText(length)) {
|
||||
String lengthDesc = getLengthDescription(length);
|
||||
sb.append("【篇幅长度】").append(lengthDesc).append("\n");
|
||||
}
|
||||
|
||||
// 序幕
|
||||
String plotIntro = request.getPlotIntro() != null ? request.getPlotIntro() : script.getPlotIntro();
|
||||
if (StringUtils.hasText(plotIntro)) {
|
||||
sb.append("【序幕-低谷回响】").append(plotIntro).append("\n");
|
||||
}
|
||||
|
||||
// 转折
|
||||
String plotTurning = request.getPlotTurning() != null ? request.getPlotTurning() : script.getPlotTurning();
|
||||
if (StringUtils.hasText(plotTurning)) {
|
||||
sb.append("【转折-契机出现】").append(plotTurning).append("\n");
|
||||
}
|
||||
|
||||
// 高潮
|
||||
String plotClimax = request.getPlotClimax() != null ? request.getPlotClimax() : script.getPlotClimax();
|
||||
if (StringUtils.hasText(plotClimax)) {
|
||||
sb.append("【高潮-命运抉择】").append(plotClimax).append("\n");
|
||||
}
|
||||
|
||||
// 结局
|
||||
String plotEnding = request.getPlotEnding() != null ? request.getPlotEnding() : script.getPlotEnding();
|
||||
if (StringUtils.hasText(plotEnding)) {
|
||||
sb.append("【结局-新的开始】").append(plotEnding).append("\n");
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpicScriptResponse selectScript(String id) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先取消所有选中
|
||||
LambdaQueryWrapper<EpicScript> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EpicScript::getUserId, currentUserId)
|
||||
.eq(EpicScript::getIsSelected, 1)
|
||||
.eq(EpicScript::getIsDeleted, 0);
|
||||
List<EpicScript> selectedScripts = this.list(wrapper);
|
||||
for (EpicScript s : selectedScripts) {
|
||||
s.setIsSelected(0);
|
||||
this.updateById(s);
|
||||
}
|
||||
|
||||
// 选中指定剧本
|
||||
EpicScript script = this.getById(id);
|
||||
if (script == null || script.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
if (!script.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
script.setIsSelected(1);
|
||||
this.updateById(script);
|
||||
return convertToResponse(script);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteScript(String id) {
|
||||
EpicScript script = this.getById(id);
|
||||
if (script == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!script.getUserId().equals(currentUserId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除关联的路径
|
||||
lifePathService.deleteByScriptId(id);
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private EpicScriptResponse convertToResponse(EpicScript script) {
|
||||
EpicScriptResponse response = new EpicScriptResponse();
|
||||
BeanUtils.copyProperties(script, response);
|
||||
response.setId(script.getId());
|
||||
response.setIsSelected(script.getIsSelected() != null && script.getIsSelected() == 1);
|
||||
if (script.getCreateTime() != null) {
|
||||
response.setCreateTime(script.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (script.getUpdateTime() != null) {
|
||||
response.setUpdateTime(script.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.growth.GrowthTopicCreateRequest;
|
||||
import com.emotion.dto.request.growth.GrowthTopicUpdateRequest;
|
||||
import com.emotion.dto.response.growth.GrowthTopicResponse;
|
||||
import com.emotion.entity.GrowthTopic;
|
||||
import com.emotion.mapper.GrowthTopicMapper;
|
||||
import com.emotion.service.GrowthTopicService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 成长话题服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Service
|
||||
public class GrowthTopicServiceImpl extends ServiceImpl<GrowthTopicMapper, GrowthTopic> implements GrowthTopicService {
|
||||
|
||||
@Override
|
||||
public IPage<GrowthTopic> getPage(BasePageRequest request) {
|
||||
Page<GrowthTopic> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<GrowthTopic> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(GrowthTopic::getTitle, request.getKeyword())
|
||||
.or().like(GrowthTopic::getDescription, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(GrowthTopic::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(GrowthTopic::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(GrowthTopic::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(GrowthTopic::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<GrowthTopicResponse> getPageWithResponse(BasePageRequest request) {
|
||||
IPage<GrowthTopic> page = getPage(request);
|
||||
List<GrowthTopicResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<GrowthTopicResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responses);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrowthTopicResponse getGrowthTopicResponseById(String id) {
|
||||
GrowthTopic topic = this.getById(id);
|
||||
if (topic == null || topic.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrowthTopic createGrowthTopic(String title, String description, String category,
|
||||
String difficultyLevel, String tags, LocalDateTime endTime) {
|
||||
GrowthTopic topic = new GrowthTopic();
|
||||
topic.setTitle(title);
|
||||
topic.setDescription(description);
|
||||
topic.setCategory(category);
|
||||
topic.setDifficulty(difficultyLevel);
|
||||
topic.setContent(description);
|
||||
topic.setDurationDays(30); // 默认30天
|
||||
topic.setIsUnlocked(1);
|
||||
topic.setProgress(BigDecimal.ZERO);
|
||||
topic.setRewards("成长积分");
|
||||
|
||||
this.save(topic);
|
||||
return topic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrowthTopicResponse createGrowthTopicWithResponse(GrowthTopicCreateRequest request) {
|
||||
GrowthTopic topic = createGrowthTopic(
|
||||
request.getTitle(),
|
||||
request.getDescription(),
|
||||
request.getCategory(),
|
||||
request.getDifficultyLevel(),
|
||||
request.getTags(),
|
||||
request.getEndTime());
|
||||
return convertToResponse(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrowthTopicResponse updateGrowthTopicWithResponse(GrowthTopicUpdateRequest request) {
|
||||
GrowthTopic topic = this.getById(request.getId());
|
||||
if (topic == null || topic.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getTitle())) {
|
||||
topic.setTitle(request.getTitle());
|
||||
}
|
||||
if (StringUtils.hasText(request.getDescription())) {
|
||||
topic.setDescription(request.getDescription());
|
||||
topic.setContent(request.getDescription());
|
||||
}
|
||||
if (StringUtils.hasText(request.getCategory())) {
|
||||
topic.setCategory(request.getCategory());
|
||||
}
|
||||
if (StringUtils.hasText(request.getDifficultyLevel())) {
|
||||
topic.setDifficulty(request.getDifficultyLevel());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTags())) {
|
||||
// GrowthTopic实体中没有tags字段,暂时忽略
|
||||
}
|
||||
if (request.getEndTime() != null) {
|
||||
// GrowthTopic实体中没有endTime字段,暂时忽略
|
||||
}
|
||||
|
||||
this.updateById(topic);
|
||||
return convertToResponse(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteGrowthTopic(String id) {
|
||||
GrowthTopic topic = this.getById(id);
|
||||
if (topic == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应对象
|
||||
*/
|
||||
private GrowthTopicResponse convertToResponse(GrowthTopic topic) {
|
||||
if (topic == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GrowthTopicResponse response = new GrowthTopicResponse();
|
||||
BeanUtils.copyProperties(topic, response);
|
||||
response.setId(topic.getId());
|
||||
if (topic.getCreateTime() != null) {
|
||||
response.setCreateTime(topic.getCreateTime().toString());
|
||||
}
|
||||
if (topic.getUpdateTime() != null) {
|
||||
response.setUpdateTime(topic.getUpdateTime().toString());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.guest.GuestUserCreateRequest;
|
||||
import com.emotion.dto.request.guest.GuestUserUpdateRequest;
|
||||
import com.emotion.dto.response.guest.GuestUserResponse;
|
||||
import com.emotion.entity.GuestUser;
|
||||
import com.emotion.mapper.GuestUserMapper;
|
||||
import com.emotion.service.GuestUserService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 访客用户服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Service
|
||||
public class GuestUserServiceImpl extends ServiceImpl<GuestUserMapper, GuestUser> implements GuestUserService {
|
||||
|
||||
@Override
|
||||
public IPage<GuestUser> getPage(BasePageRequest request) {
|
||||
Page<GuestUser> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(GuestUser::getNickname, request.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.eq(GuestUser::getIsDeleted, 0).orderByDesc(GuestUser::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<GuestUserResponse> getPageWithResponse(BasePageRequest request) {
|
||||
IPage<GuestUser> page = getPage(request);
|
||||
List<GuestUserResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<GuestUserResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responses);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUserResponse getGuestUserResponseById(String id) {
|
||||
GuestUser guestUser = this.getById(id);
|
||||
if (guestUser == null || guestUser.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(guestUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUser getByDeviceId(String deviceId) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.like(GuestUser::getDeviceInfo, deviceId)
|
||||
.eq(GuestUser::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByIpAddress(String ipAddress) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GuestUser::getIpAddress, ipAddress)
|
||||
.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByDesc(GuestUser::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByUserAgent(String userAgent) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GuestUser::getUserAgent, userAgent)
|
||||
.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByDesc(GuestUser::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByStatus(String status) {
|
||||
// GuestUser实体中没有status字段,暂时返回空列表
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(GuestUser::getCreateTime, startTime, endTime)
|
||||
.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByDesc(GuestUser::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByLastActiveTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(GuestUser::getLastActiveTime, startTime, endTime)
|
||||
.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByDesc(GuestUser::getLastActiveTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByStatus(String status) {
|
||||
// GuestUser实体中没有status字段,暂时返回0
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByIpAddress(String ipAddress) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GuestUser::getIpAddress, ipAddress)
|
||||
.eq(GuestUser::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countTodayNewGuests() {
|
||||
LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
LocalDateTime tomorrow = today.plusDays(1);
|
||||
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(GuestUser::getCreateTime, today, tomorrow)
|
||||
.eq(GuestUser::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countActiveGuests(Integer days) {
|
||||
LocalDateTime activeTime = LocalDateTime.now().minusDays(days);
|
||||
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.ge(GuestUser::getLastActiveTime, activeTime)
|
||||
.eq(GuestUser::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getRecentVisitors(Integer limit) {
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByDesc(GuestUser::getLastActiveTime)
|
||||
.last("LIMIT " + limit);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getInactiveGuests(Integer days) {
|
||||
LocalDateTime inactiveTime = LocalDateTime.now().minusDays(days);
|
||||
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.lt(GuestUser::getLastActiveTime, inactiveTime)
|
||||
.eq(GuestUser::getIsDeleted, 0)
|
||||
.orderByAsc(GuestUser::getLastActiveTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuestUser> getByVisitCountRange(Integer minVisits, Integer maxVisits) {
|
||||
// GuestUser实体中没有visitCount字段,暂时返回空列表
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getAvgVisitCount() {
|
||||
// 这里需要自定义SQL查询平均值,暂时返回0
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateLastActiveTime(String id, LocalDateTime lastActiveTime) {
|
||||
GuestUser guestUser = new GuestUser();
|
||||
guestUser.setId(id);
|
||||
guestUser.setLastActiveTime(lastActiveTime);
|
||||
return this.updateById(guestUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementVisitCount(String id) {
|
||||
// GuestUser实体中没有visitCount字段,暂时返回false
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String id, String status) {
|
||||
// GuestUser实体中没有status字段,暂时返回false
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUser getOrCreateByDeviceInfo(String deviceId, String ipAddress, String userAgent) {
|
||||
// 先尝试根据设备信息查找
|
||||
GuestUser existingUser = getByDeviceId(deviceId);
|
||||
if (existingUser != null) {
|
||||
// 更新最后活跃时间
|
||||
updateLastActiveTime(existingUser.getId(), LocalDateTime.now());
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
// 如果不存在,创建新的访客用户
|
||||
return createGuestUser(deviceId, ipAddress, userAgent, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cleanExpiredGuests(Integer days) {
|
||||
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
|
||||
|
||||
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.lt(GuestUser::getLastActiveTime, expireTime);
|
||||
|
||||
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
|
||||
return this.remove(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUser createGuestUser(String deviceId, String ipAddress, String userAgent, String location) {
|
||||
GuestUser guestUser = new GuestUser();
|
||||
guestUser.setGuestUserId("guest_" + System.currentTimeMillis());
|
||||
guestUser.setIpAddress(ipAddress);
|
||||
guestUser.setUserAgent(userAgent);
|
||||
guestUser.setNickname("访客用户");
|
||||
guestUser.setAvatar("default_avatar.png");
|
||||
guestUser.setLastActiveTime(LocalDateTime.now());
|
||||
guestUser.setConversationCount(0);
|
||||
guestUser.setMessageCount(0);
|
||||
guestUser.setLocation(location);
|
||||
guestUser.setDeviceInfo(deviceId);
|
||||
|
||||
this.save(guestUser);
|
||||
return guestUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUserResponse createGuestUserWithResponse(GuestUserCreateRequest request) {
|
||||
GuestUser guestUser = createGuestUser(
|
||||
request.getDeviceId(),
|
||||
request.getIpAddress(),
|
||||
request.getUserAgent(),
|
||||
request.getLocation());
|
||||
return convertToResponse(guestUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUserResponse updateGuestUserWithResponse(GuestUserUpdateRequest request) {
|
||||
GuestUser guestUser = this.getById(request.getId());
|
||||
if (guestUser == null || guestUser.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getNickname())) {
|
||||
guestUser.setNickname(request.getNickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAvatar())) {
|
||||
guestUser.setAvatar(request.getAvatar());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLocation())) {
|
||||
guestUser.setLocation(request.getLocation());
|
||||
}
|
||||
|
||||
this.updateById(guestUser);
|
||||
return convertToResponse(guestUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteGuestUser(String id) {
|
||||
GuestUser guestUser = this.getById(id);
|
||||
if (guestUser == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应对象
|
||||
*/
|
||||
private GuestUserResponse convertToResponse(GuestUser guestUser) {
|
||||
if (guestUser == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GuestUserResponse response = new GuestUserResponse();
|
||||
BeanUtils.copyProperties(guestUser, response);
|
||||
response.setId(guestUser.getId());
|
||||
if (guestUser.getCreateTime() != null) {
|
||||
response.setCreateTime(guestUser.getCreateTime().toString());
|
||||
}
|
||||
if (guestUser.getUpdateTime() != null) {
|
||||
response.setUpdateTime(guestUser.getUpdateTime().toString());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.service.TtsEngineClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class HttpTtsEngineClient implements TtsEngineClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String engineUrl;
|
||||
|
||||
public HttpTtsEngineClient(RestTemplateBuilder restTemplateBuilder,
|
||||
@Value("${emotion.tts.engine-url:http://127.0.0.1:19110}") String engineUrl,
|
||||
@Value("${emotion.tts.engine-connect-timeout-ms:5000}") long connectTimeoutMs,
|
||||
@Value("${emotion.tts.engine-read-timeout-ms:240000}") long readTimeoutMs) {
|
||||
this.engineUrl = normalizeEngineUrl(engineUrl);
|
||||
this.restTemplate = restTemplateBuilder
|
||||
.setConnectTimeout(Duration.ofMillis(connectTimeoutMs))
|
||||
.setReadTimeout(Duration.ofMillis(readTimeoutMs))
|
||||
.defaultHeader("User-Agent", "EmotionMuseum-TTS/1.0")
|
||||
.defaultHeader("Accept", "application/json, text/plain, */*")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TtsEngineResult synthesize(String text, String voice, String outputPath, SynthesisOptions options) {
|
||||
try {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("text", text);
|
||||
body.put("voice", voice);
|
||||
body.put("outputPath", outputPath);
|
||||
if (options != null) {
|
||||
if (options.getSpeechRate() != null) {
|
||||
body.put("speechRate", options.getSpeechRate());
|
||||
}
|
||||
if (options.getPitch() != null) {
|
||||
body.put("pitch", options.getPitch());
|
||||
}
|
||||
if (StringUtils.hasText(options.getEmotion())) {
|
||||
body.put("emotion", options.getEmotion());
|
||||
}
|
||||
}
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(engineUrl + "/synthesize", body, Map.class);
|
||||
Map<?, ?> data = response.getBody();
|
||||
boolean success = data != null && Boolean.TRUE.equals(data.get("success"));
|
||||
if (!success) {
|
||||
String message = data == null ? "empty response" : String.valueOf(data.get("errorMessage"));
|
||||
if (hasGeneratedAudio(outputPath)) {
|
||||
return new TtsEngineResult(true, outputPath, null, null);
|
||||
}
|
||||
return new TtsEngineResult(false, null, null, message);
|
||||
}
|
||||
Long durationMs = data.get("durationMs") instanceof Number
|
||||
? ((Number) data.get("durationMs")).longValue()
|
||||
: null;
|
||||
return new TtsEngineResult(true, String.valueOf(data.get("audioPath")), durationMs, null);
|
||||
} catch (Exception e) {
|
||||
if (hasGeneratedAudio(outputPath)) {
|
||||
return new TtsEngineResult(true, outputPath, null, null);
|
||||
}
|
||||
return new TtsEngineResult(false, null, null, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeEngineUrl(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return "http://127.0.0.1:19110";
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.endsWith("/") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;
|
||||
}
|
||||
|
||||
private static boolean hasGeneratedAudio(String outputPath) {
|
||||
if (!StringUtils.hasText(outputPath)) {
|
||||
return false;
|
||||
}
|
||||
File file = new File(outputPath);
|
||||
return file.isFile() && file.length() > 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.LifeEventCreateRequest;
|
||||
import com.emotion.dto.request.LifeEventPageRequest;
|
||||
import com.emotion.dto.request.LifeEventUpdateRequest;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.LifeEventResponse;
|
||||
import com.emotion.entity.LifeEvent;
|
||||
import com.emotion.mapper.LifeEventMapper;
|
||||
import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.LifeEventService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 生命事件服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent>
|
||||
implements LifeEventService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
|
||||
private static final DateTimeFormatter DATE_ONLY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
|
||||
@Autowired
|
||||
private AiRuntimeService aiRuntimeService;
|
||||
|
||||
@Override
|
||||
public PageResult<LifeEventResponse> getPageByCurrentUser(LifeEventPageRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return new PageResult<>();
|
||||
}
|
||||
|
||||
Page<LifeEvent> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifeEvent::getUserId, currentUserId)
|
||||
.eq(LifeEvent::getIsDeleted, 0);
|
||||
|
||||
// 事件类型筛选
|
||||
if (StringUtils.hasText(request.getEventType())) {
|
||||
wrapper.eq(LifeEvent::getEventType, request.getEventType());
|
||||
}
|
||||
|
||||
// 情绪类型筛选
|
||||
if (StringUtils.hasText(request.getEmotionType())) {
|
||||
wrapper.eq(LifeEvent::getEmotionType, request.getEmotionType());
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(LifeEvent::getTitle, request.getKeyword())
|
||||
.or().like(LifeEvent::getContent, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 按事件日期倒序排列
|
||||
wrapper.orderByDesc(LifeEvent::getEventDate);
|
||||
|
||||
Page<LifeEvent> resultPage = this.page(page, wrapper);
|
||||
List<LifeEventResponse> responses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<LifeEventResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LifeEventResponse> getListByCurrentUser() {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifeEvent::getUserId, currentUserId)
|
||||
.eq(LifeEvent::getIsDeleted, 0)
|
||||
.orderByDesc(LifeEvent::getEventDate);
|
||||
|
||||
return this.list(wrapper).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifeEventResponse getEventById(String id) {
|
||||
LifeEvent event = this.getById(id);
|
||||
if (event == null || event.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!event.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToResponse(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifeEventResponse createEvent(LifeEventCreateRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LifeEvent event = new LifeEvent();
|
||||
event.setUserId(currentUserId);
|
||||
event.setEventType(StringUtils.hasText(request.getEventType()) ? request.getEventType() : "daily_log");
|
||||
event.setTitle(request.getTitle());
|
||||
event.setContent(request.getContent());
|
||||
event.setAiReply(request.getAiReply());
|
||||
event.setEmotionType(request.getEmotionType());
|
||||
event.setTags(request.getTags());
|
||||
event.setTimeMode(StringUtils.hasText(request.getTimeMode()) ? request.getTimeMode() : "date");
|
||||
event.setEventDateText(StringUtils.hasText(request.getEventDateText()) ? request.getEventDateText() : request.getEventDate());
|
||||
|
||||
// 解析事件日期,支持多种格式
|
||||
event.setEventDate(parseEventDate(request.getEventDate(), event.getTimeMode(), event.getEventDateText()));
|
||||
event.setEventEndDate(parseEventEndDate(request.getEventEndDate(), event.getTimeMode()));
|
||||
|
||||
// 情绪评分
|
||||
if (request.getEmotionScore() != null) {
|
||||
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(event.getAiReply())) {
|
||||
String aiGeneratedContent = generateHealingByAi(event);
|
||||
if (StringUtils.hasText(aiGeneratedContent)) {
|
||||
event.setAiReply(aiGeneratedContent);
|
||||
}
|
||||
}
|
||||
|
||||
this.save(event);
|
||||
return convertToResponse(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Coze AI生成疗愈内容
|
||||
*
|
||||
* @param event 人生事件
|
||||
* @return AI生成的疗愈内容,失败时返回null
|
||||
*/
|
||||
private String generateHealingByAi(LifeEvent event) {
|
||||
try {
|
||||
// 组装AI输入
|
||||
String input = assembleHealingInput(event);
|
||||
log.info("开始调用AI生成疗愈回复,用户ID: {}, 输入长度: {}", event.getUserId(), input.length());
|
||||
|
||||
JSONObject inputs = new JSONObject();
|
||||
inputs.put("mode", "life_event_analysis");
|
||||
inputs.put("input", input);
|
||||
inputs.put("message", input);
|
||||
inputs.put("content", event.getContent());
|
||||
inputs.put("title", event.getTitle());
|
||||
inputs.put("emotionType", event.getEmotionType());
|
||||
inputs.put("eventType", event.getEventType());
|
||||
inputs.put("eventDate", event.getEventDateText());
|
||||
|
||||
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
|
||||
runtimeRequest.setSceneCode("life_healing");
|
||||
runtimeRequest.setUserId(event.getUserId());
|
||||
runtimeRequest.setInputs(inputs);
|
||||
|
||||
AiRuntimeTestResponse response = aiRuntimeService.test(runtimeRequest);
|
||||
String result = response != null ? response.getOutput() : null;
|
||||
|
||||
log.info("AI生成疗愈回复完成,用户ID: {}, 结果长度: {}", event.getUserId(), result != null ? result.length() : 0);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI生成疗愈回复失败,用户ID: {}, 错误: {}", event.getUserId(), e.getMessage(), e);
|
||||
// AI调用失败不影响事件创建,返回null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装AI疗愈输入内容
|
||||
*
|
||||
* @param event 人生事件
|
||||
* @return 格式化的输入字符串
|
||||
*/
|
||||
private String assembleHealingInput(LifeEvent event) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 标题
|
||||
if (StringUtils.hasText(event.getTitle())) {
|
||||
sb.append("【事件标题】").append(event.getTitle()).append("\n");
|
||||
}
|
||||
|
||||
// 发生时间
|
||||
if (!ObjectUtils.isEmpty(event.getEventDate())) {
|
||||
sb.append("【发生时间】").append(event.getEventDate()).append("\n");
|
||||
}
|
||||
|
||||
// 经历详解
|
||||
if (StringUtils.hasText(event.getContent())) {
|
||||
sb.append("【经历详解】").append(event.getContent()).append("\n");
|
||||
}
|
||||
|
||||
// 情绪类型
|
||||
if (StringUtils.hasText(event.getEmotionType())) {
|
||||
sb.append("【情绪类型】").append(event.getEmotionType()).append("\n");
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifeEventResponse updateEvent(LifeEventUpdateRequest request) {
|
||||
LifeEvent event = this.getById(request.getId());
|
||||
if (event == null || event.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!event.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean hasClientAiReply = request.getAiReply() != null;
|
||||
|
||||
// 更新字段
|
||||
if (StringUtils.hasText(request.getEventType())) {
|
||||
event.setEventType(request.getEventType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTitle())) {
|
||||
event.setTitle(request.getTitle());
|
||||
}
|
||||
if (StringUtils.hasText(request.getContent())) {
|
||||
event.setContent(request.getContent());
|
||||
}
|
||||
if (request.getAiReply() != null) {
|
||||
event.setAiReply(request.getAiReply());
|
||||
}
|
||||
if (request.getEmotionType() != null) {
|
||||
event.setEmotionType(request.getEmotionType());
|
||||
}
|
||||
if (request.getTags() != null) {
|
||||
event.setTags(request.getTags());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTimeMode())) {
|
||||
event.setTimeMode(request.getTimeMode());
|
||||
}
|
||||
if (request.getEventDateText() != null) {
|
||||
event.setEventDateText(request.getEventDateText());
|
||||
}
|
||||
if (StringUtils.hasText(request.getEventDate())) {
|
||||
event.setEventDate(parseEventDate(request.getEventDate(), event.getTimeMode(), event.getEventDateText()));
|
||||
}
|
||||
if (request.getEventEndDate() != null) {
|
||||
event.setEventEndDate(parseEventEndDate(request.getEventEndDate(), event.getTimeMode()));
|
||||
}
|
||||
if (request.getEmotionScore() != null) {
|
||||
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
|
||||
}
|
||||
|
||||
if (!hasClientAiReply) {
|
||||
String aiGeneratedContent = generateHealingByAi(event);
|
||||
if (StringUtils.hasText(aiGeneratedContent)) {
|
||||
event.setAiReply(aiGeneratedContent);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateById(event);
|
||||
return convertToResponse(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteEvent(String id) {
|
||||
LifeEvent event = this.getById(id);
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!event.getUserId().equals(currentUserId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private LifeEventResponse convertToResponse(LifeEvent event) {
|
||||
LifeEventResponse response = new LifeEventResponse();
|
||||
BeanUtils.copyProperties(event, response);
|
||||
response.setId(event.getId());
|
||||
if (event.getEventDate() != null) {
|
||||
response.setEventDate(event.getEventDate().format(ISO_FORMATTER));
|
||||
}
|
||||
if (event.getEventEndDate() != null) {
|
||||
response.setEventEndDate(event.getEventEndDate().format(ISO_FORMATTER));
|
||||
}
|
||||
if (event.getEmotionScore() != null) {
|
||||
response.setEmotionScore(event.getEmotionScore().doubleValue());
|
||||
}
|
||||
if (event.getCreateTime() != null) {
|
||||
response.setCreateTime(event.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (event.getUpdateTime() != null) {
|
||||
response.setUpdateTime(event.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析事件日期,支持多种格式
|
||||
* 支持格式:yyyy-MM-dd、yyyy-MM-ddTHH:mm:ss、ISO_DATE_TIME
|
||||
*
|
||||
* @param dateStr 日期字符串
|
||||
* @return 解析后的LocalDateTime,解析失败返回当前时间
|
||||
*/
|
||||
private LocalDateTime parseEventDate(String dateStr, String timeMode, String eventDateText) {
|
||||
String source = StringUtils.hasText(dateStr) ? dateStr : eventDateText;
|
||||
if (!StringUtils.hasText(source)) {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
|
||||
if ("month".equals(timeMode)) {
|
||||
try {
|
||||
return YearMonth.parse(source.substring(0, 7), YEAR_MONTH_FORMATTER).atDay(1).atStartOfDay();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if ("season".equals(timeMode)) {
|
||||
LocalDate seasonDate = parseSeasonStart(source);
|
||||
if (seasonDate != null) {
|
||||
return seasonDate.atStartOfDay();
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试ISO格式 (yyyy-MM-ddTHH:mm:ss.SSSZ)
|
||||
try {
|
||||
return LocalDateTime.parse(source, ISO_FORMATTER);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 尝试日期时间格式 (yyyy-MM-dd HH:mm:ss)
|
||||
try {
|
||||
return LocalDateTime.parse(source, DATE_TIME_FORMATTER);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 尝试纯日期格式 (yyyy-MM-dd),时间设为当天开始
|
||||
try {
|
||||
return LocalDate.parse(source, DATE_ONLY_FORMATTER).atStartOfDay();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 所有格式都失败,返回当前时间
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
|
||||
private LocalDateTime parseEventEndDate(String dateStr, String timeMode) {
|
||||
if (!"range".equals(timeMode) || !StringUtils.hasText(dateStr)) {
|
||||
return null;
|
||||
}
|
||||
return parseEventDate(dateStr, "date", dateStr);
|
||||
}
|
||||
|
||||
private LocalDate parseSeasonStart(String value) {
|
||||
try {
|
||||
String[] parts = value.split("-");
|
||||
int year = Integer.parseInt(parts[0]);
|
||||
String season = parts.length > 1 ? parts[1] : "spring";
|
||||
int month;
|
||||
switch (season) {
|
||||
case "summer":
|
||||
month = 6;
|
||||
break;
|
||||
case "autumn":
|
||||
month = 9;
|
||||
break;
|
||||
case "winter":
|
||||
month = 12;
|
||||
break;
|
||||
case "spring":
|
||||
default:
|
||||
month = 3;
|
||||
break;
|
||||
}
|
||||
return LocalDate.of(year, month, 1);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.LifePathCreateRequest;
|
||||
import com.emotion.dto.request.LifePathPageRequest;
|
||||
import com.emotion.dto.request.LifePathUpdateRequest;
|
||||
import com.emotion.dto.response.LifePathResponse;
|
||||
import com.emotion.entity.LifePath;
|
||||
import com.emotion.mapper.LifePathMapper;
|
||||
import com.emotion.service.LifePathService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 实现路径服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@Service
|
||||
public class LifePathServiceImpl extends ServiceImpl<LifePathMapper, LifePath>
|
||||
implements LifePathService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<LifePathResponse> getPageByCurrentUser(LifePathPageRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return new PageResult<>();
|
||||
}
|
||||
|
||||
Page<LifePath> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifePath::getUserId, currentUserId)
|
||||
.eq(LifePath::getIsDeleted, 0);
|
||||
|
||||
// 剧本ID筛选
|
||||
if (StringUtils.hasText(request.getScriptId())) {
|
||||
wrapper.eq(LifePath::getScriptId, request.getScriptId());
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
wrapper.eq(LifePath::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
// 按创建时间倒序排列
|
||||
wrapper.orderByDesc(LifePath::getCreateTime);
|
||||
|
||||
Page<LifePath> resultPage = this.page(page, wrapper);
|
||||
List<LifePathResponse> responses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<LifePathResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LifePathResponse> getListByCurrentUser() {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifePath::getUserId, currentUserId)
|
||||
.eq(LifePath::getIsDeleted, 0)
|
||||
.orderByDesc(LifePath::getCreateTime);
|
||||
|
||||
return this.list(wrapper).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifePathResponse getByScriptId(String scriptId) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifePath::getUserId, currentUserId)
|
||||
.eq(LifePath::getScriptId, scriptId)
|
||||
.eq(LifePath::getIsDeleted, 0);
|
||||
|
||||
LifePath path = this.getOne(wrapper);
|
||||
return path != null ? convertToResponse(path) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifePathResponse getPathById(String id) {
|
||||
LifePath path = this.getById(id);
|
||||
if (path == null || path.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!path.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return convertToResponse(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifePathResponse createPath(LifePathCreateRequest request) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LifePath path = new LifePath();
|
||||
path.setUserId(currentUserId);
|
||||
path.setScriptId(request.getScriptId());
|
||||
path.setTitle(request.getTitle());
|
||||
path.setDescription(request.getDescription());
|
||||
path.setSteps(request.getSteps());
|
||||
path.setStatus(StringUtils.hasText(request.getStatus()) ? request.getStatus() : "active");
|
||||
path.setProgress(request.getProgress() != null ? BigDecimal.valueOf(request.getProgress()) : BigDecimal.ZERO);
|
||||
|
||||
this.save(path);
|
||||
return convertToResponse(path);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LifePathResponse updatePath(LifePathUpdateRequest request) {
|
||||
LifePath path = this.getById(request.getId());
|
||||
if (path == null || path.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!path.getUserId().equals(currentUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (StringUtils.hasText(request.getTitle())) {
|
||||
path.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getDescription() != null) {
|
||||
path.setDescription(request.getDescription());
|
||||
}
|
||||
if (request.getSteps() != null) {
|
||||
path.setSteps(request.getSteps());
|
||||
}
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
path.setStatus(request.getStatus());
|
||||
}
|
||||
if (request.getProgress() != null) {
|
||||
path.setProgress(BigDecimal.valueOf(request.getProgress()));
|
||||
}
|
||||
|
||||
this.updateById(path);
|
||||
return convertToResponse(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deletePath(String id) {
|
||||
LifePath path = this.getById(id);
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!path.getUserId().equals(currentUserId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteByScriptId(String scriptId) {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifePath::getUserId, currentUserId)
|
||||
.eq(LifePath::getScriptId, scriptId);
|
||||
|
||||
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
|
||||
return this.remove(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private LifePathResponse convertToResponse(LifePath path) {
|
||||
LifePathResponse response = new LifePathResponse();
|
||||
BeanUtils.copyProperties(path, response);
|
||||
response.setId(path.getId());
|
||||
if (path.getProgress() != null) {
|
||||
response.setProgress(path.getProgress().doubleValue());
|
||||
}
|
||||
if (path.getCreateTime() != null) {
|
||||
response.setCreateTime(path.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (path.getUpdateTime() != null) {
|
||||
response.setUpdateTime(path.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.MessagePageRequest;
|
||||
import com.emotion.dto.request.MessageSearchRequest;
|
||||
import com.emotion.dto.request.MessageRecentRequest;
|
||||
import com.emotion.dto.request.MessageCreateRequest;
|
||||
import com.emotion.dto.response.MessageResponse;
|
||||
import com.emotion.entity.Message;
|
||||
import com.emotion.mapper.MessageMapper;
|
||||
import com.emotion.service.MessageService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 消息服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public IPage<Message> getPage(BasePageRequest request) {
|
||||
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(Message::getContent, request.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.eq(Message::getIsDeleted, 0).orderByDesc(Message::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MessageResponse> getPageWithResponse(MessagePageRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 调用用户消息分页查询方法
|
||||
PageResult<MessageResponse> pageResult = getUserMessagesWithPage(request);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId) {
|
||||
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getBySender(String sender) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getSender, sender)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.between(Message::getCreateTime, startTime, endTime)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message getLastMessageByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getMessageOrder)
|
||||
.last("LIMIT 1");
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getRepliesByParentId(String parentMessageId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getParentMessageId, parentMessageId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countBySender(String sender) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getSender, sender)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countUnreadMessages(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsRead, 0)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String messageId, String status) {
|
||||
Message message = new Message();
|
||||
message.setId(messageId);
|
||||
message.setStatus(status);
|
||||
return this.updateById(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateReadStatus(String messageId, Integer isRead) {
|
||||
Message message = new Message();
|
||||
message.setId(messageId);
|
||||
message.setIsRead(isRead);
|
||||
return this.updateById(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markConversationMessagesAsRead(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsRead, 0)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
|
||||
Message updateMessage = new Message();
|
||||
updateMessage.setIsRead(1);
|
||||
|
||||
return this.update(updateMessage, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message createMessage(Message message) {
|
||||
// 设置默认值
|
||||
if (message.getTimestamp() == null) {
|
||||
message.setTimestamp(LocalDateTime.now());
|
||||
}
|
||||
if (message.getStatus() == null) {
|
||||
message.setStatus("sent");
|
||||
}
|
||||
if (message.getIsRead() == null) {
|
||||
message.setIsRead(0);
|
||||
}
|
||||
|
||||
// 设置消息顺序 - 在同一个会话中递增
|
||||
if (message.getMessageOrder() == null) {
|
||||
Long nextOrder = getNextMessageOrder(message.getConversationId());
|
||||
message.setMessageOrder(nextOrder);
|
||||
}
|
||||
|
||||
this.save(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话中下一个消息顺序
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 下一个消息顺序
|
||||
*/
|
||||
private Long getNextMessageOrder(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getMessageOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
Message lastMessage = this.getOne(wrapper);
|
||||
if (lastMessage != null && lastMessage.getMessageOrder() != null) {
|
||||
return lastMessage.getMessageOrder() + 1;
|
||||
}
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markAsRead(String messageId) {
|
||||
return updateReadStatus(messageId, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
// 使用 MyBatis-Plus 条件构造器,直接根据消息表的 user_id 字段查询
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getUserId, userId)
|
||||
.between(Message::getCreateTime, startTime, endTime)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size) {
|
||||
// 使用 MyBatis-Plus 分页 + 条件构造器
|
||||
Page<Message> page = new Page<>(current, size);
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getUserId, userId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit) {
|
||||
// 使用 MyBatis-Plus 分页 + 条件构造器,避免硬编码 SQL
|
||||
Page<Message> page = new Page<>(1, limit);
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getUserId, userId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.like(StringUtils.hasText(keyword), Message::getContent, keyword)
|
||||
.orderByAsc(Message::getMessageOrder);
|
||||
IPage<Message> result = this.page(page, wrapper);
|
||||
return result.getRecords();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getRecentByUserId(String userId, Integer limit) {
|
||||
// 使用 MyBatis-Plus 分页查询最近消息
|
||||
Page<Message> page = new Page<>(1, limit);
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getUserId, userId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getMessageOrder);
|
||||
IPage<Message> result = this.page(page, wrapper);
|
||||
return result.getRecords();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MessageResponse> getUserMessagesWithPage(MessagePageRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 调用原有的分页查询方法
|
||||
IPage<Message> page = getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()),
|
||||
Math.toIntExact(request.getSize()));
|
||||
|
||||
// 转换为响应对象
|
||||
List<MessageResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建分页结果
|
||||
PageResult<MessageResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageResponse> searchUserMessages(MessageSearchRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 调用原有的搜索方法
|
||||
List<Message> messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit());
|
||||
|
||||
// 转换为响应对象
|
||||
return messages.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageResponse> getUserRecentMessages(MessageRecentRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 调用原有的获取最近消息方法
|
||||
List<Message> messages = getRecentByUserId(userId, request.getLimit());
|
||||
|
||||
// 转换为响应对象
|
||||
return messages.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageResponse createMessageFromRequest(MessageCreateRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 构建消息对象
|
||||
Message message = new Message();
|
||||
message.setConversationId(request.getConversationId());
|
||||
message.setCreateBy(userId);
|
||||
message.setContent(request.getContent());
|
||||
message.setType(request.getContentType());
|
||||
message.setSender(request.getSenderType());
|
||||
|
||||
// 调用原有的创建方法
|
||||
Message savedMessage = createMessage(message);
|
||||
|
||||
// 转换为响应对象
|
||||
return convertToResponse(savedMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageResponse getMessageById(String id) {
|
||||
Message message = getById(id);
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
return convertToResponse(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MessageResponse> searchWithResponse(MessageSearchRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 构建分页对象
|
||||
Page<Message> page = new Page<>(1L, request.getLimit().longValue());
|
||||
|
||||
// 调用搜索方法
|
||||
List<Message> messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit());
|
||||
|
||||
// 设置分页信息
|
||||
page.setRecords(messages);
|
||||
page.setTotal(messages.size());
|
||||
|
||||
// 转换为响应对象
|
||||
List<MessageResponse> responses = messages.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建分页结果
|
||||
PageResult<MessageResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responses);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MessageResponse> getRecentWithResponse(MessageRecentRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
// 构建分页对象
|
||||
Page<Message> page = new Page<>(1L, request.getLimit().longValue());
|
||||
|
||||
// 调用获取最近消息方法
|
||||
List<Message> messages = getRecentByUserId(userId, request.getLimit());
|
||||
|
||||
// 设置分页信息
|
||||
page.setRecords(messages);
|
||||
page.setTotal(messages.size());
|
||||
|
||||
// 转换为响应对象
|
||||
List<MessageResponse> responses = messages.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建分页结果
|
||||
PageResult<MessageResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responses);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageResponse updateMessage(String id, String content) {
|
||||
Message message = this.getById(id);
|
||||
if (message == null || message.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
message.setContent(content);
|
||||
this.updateById(message);
|
||||
|
||||
// 转换为响应对象
|
||||
return convertToResponse(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteMessage(String id) {
|
||||
Message message = this.getById(id);
|
||||
if (message == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private MessageResponse convertToResponse(Message message) {
|
||||
MessageResponse response = new MessageResponse();
|
||||
BeanUtils.copyProperties(message, response);
|
||||
response.setId(message.getId());
|
||||
if (message.getCreateTime() != null) {
|
||||
response.setCreateTime(message.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (message.getUpdateTime() != null) {
|
||||
response.setUpdateTime(message.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.reward.RewardCreateRequest;
|
||||
import com.emotion.dto.request.reward.RewardUpdateRequest;
|
||||
import com.emotion.dto.response.reward.RewardResponse;
|
||||
import com.emotion.entity.Reward;
|
||||
import com.emotion.mapper.RewardMapper;
|
||||
import com.emotion.service.RewardService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 奖励服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Service
|
||||
public class RewardServiceImpl extends ServiceImpl<RewardMapper, Reward> implements RewardService {
|
||||
|
||||
@Override
|
||||
public IPage<Reward> getPage(BasePageRequest request) {
|
||||
Page<Reward> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(Reward::getName, request.getKeyword())
|
||||
.or().like(Reward::getDescription, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(Reward::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(Reward::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(Reward::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(Reward::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<RewardResponse> getPageWithResponse(BasePageRequest request) {
|
||||
IPage<Reward> page = getPage(request);
|
||||
List<RewardResponse> responses = page.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
PageResult<RewardResponse> pageResult = new PageResult<>();
|
||||
pageResult.setRecords(responses);
|
||||
pageResult.setCurrent(page.getCurrent());
|
||||
pageResult.setSize(page.getSize());
|
||||
pageResult.setTotal(page.getTotal());
|
||||
pageResult.setPages(page.getPages());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Reward> getPageByUserId(BasePageRequest request, String userId) {
|
||||
Page<Reward> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RewardResponse getRewardResponseById(String id) {
|
||||
Reward reward = this.getById(id);
|
||||
if (reward == null || reward.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(reward);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByUserId(String userId) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByRewardType(String rewardType) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getType, rewardType)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByUserIdAndRewardType(String userId, String rewardType) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getType, rewardType)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByStatus(String status) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
// 由于Reward实体中没有明确的status字段,这里返回空列表
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByUserIdAndStatus(String userId, String status) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByPointsRange(Integer minPoints, Integer maxPoints) {
|
||||
// 这里需要根据实际的points字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(Reward::getCreateTime, startTime, endTime)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getByEarnedTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(Reward::getEarnedTime, startTime, endTime)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getEarnedTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserId(String userId) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByRewardType(String rewardType) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getType, rewardType)
|
||||
.eq(Reward::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserIdAndRewardType(String userId, String rewardType) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getType, rewardType)
|
||||
.eq(Reward::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByStatus(String status) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer sumPointsByUserId(String userId) {
|
||||
// 这里需要根据实际的points字段来实现
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer sumPointsByUserIdAndRewardType(String userId, String rewardType) {
|
||||
// 这里需要根据实际的points字段来实现
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getRecentByUserId(String userId, Integer limit) {
|
||||
LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Reward::getCreateBy, userId)
|
||||
.eq(Reward::getIsDeleted, 0)
|
||||
.orderByDesc(Reward::getCreateTime)
|
||||
.last("LIMIT " + limit);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getHighPointsRewards(Integer minPoints) {
|
||||
// 这里需要根据实际的points字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getPendingRewardsByUserId(String userId) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getClaimedRewardsByUserId(String userId) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getExpiredRewards() {
|
||||
// 这里需要根据实际的过期时间字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String id, String status, LocalDateTime claimedTime) {
|
||||
// 这里需要根据实际的status字段来实现
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateExpiredRewards() {
|
||||
// 这里需要根据实际的过期时间字段来实现
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Reward> getBySource(String source) {
|
||||
// 这里需要根据实际的source字段来实现
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reward createReward(String userId, String rewardType, String value,
|
||||
String description, String topicId, String achievementId, String icon, String rarity) {
|
||||
Reward reward = new Reward();
|
||||
reward.setType(rewardType);
|
||||
reward.setName(rewardType + "奖励");
|
||||
reward.setValue(value);
|
||||
reward.setDescription(description);
|
||||
reward.setTopicId(topicId);
|
||||
reward.setAchievementId(achievementId);
|
||||
reward.setIcon(icon);
|
||||
reward.setRarity(rarity);
|
||||
reward.setCreateBy(userId);
|
||||
reward.setEarnedTime(LocalDateTime.now());
|
||||
reward.setIsNew(1);
|
||||
|
||||
this.save(reward);
|
||||
return reward;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RewardResponse createRewardWithResponse(RewardCreateRequest request) {
|
||||
Reward reward = createReward(
|
||||
request.getUserId(),
|
||||
request.getRewardType(),
|
||||
request.getValue(),
|
||||
request.getDescription(),
|
||||
request.getTopicId(),
|
||||
request.getAchievementId(),
|
||||
request.getIcon(),
|
||||
request.getRarity());
|
||||
return convertToResponse(reward);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RewardResponse updateRewardWithResponse(RewardUpdateRequest request) {
|
||||
Reward reward = this.getById(request.getId());
|
||||
if (reward == null || reward.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getRewardType())) {
|
||||
reward.setType(request.getRewardType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getValue())) {
|
||||
reward.setValue(request.getValue());
|
||||
}
|
||||
if (StringUtils.hasText(request.getDescription())) {
|
||||
reward.setDescription(request.getDescription());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTopicId())) {
|
||||
reward.setTopicId(request.getTopicId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAchievementId())) {
|
||||
reward.setAchievementId(request.getAchievementId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getIcon())) {
|
||||
reward.setIcon(request.getIcon());
|
||||
}
|
||||
if (StringUtils.hasText(request.getRarity())) {
|
||||
reward.setRarity(request.getRarity());
|
||||
}
|
||||
if (request.getIsNew() != null) {
|
||||
reward.setIsNew(request.getIsNew());
|
||||
}
|
||||
|
||||
this.updateById(reward);
|
||||
return convertToResponse(reward);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteReward(String id) {
|
||||
Reward reward = this.getById(id);
|
||||
if (reward == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为响应对象
|
||||
*/
|
||||
private RewardResponse convertToResponse(Reward reward) {
|
||||
if (reward == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RewardResponse response = new RewardResponse();
|
||||
BeanUtils.copyProperties(reward, response);
|
||||
response.setId(reward.getId());
|
||||
if (reward.getCreateTime() != null) {
|
||||
response.setCreateTime(reward.getCreateTime().toString());
|
||||
}
|
||||
if (reward.getUpdateTime() != null) {
|
||||
response.setUpdateTime(reward.getUpdateTime().toString());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.emotion.entity.SocialProfileInsight;
|
||||
import com.emotion.mapper.SocialProfileInsightMapper;
|
||||
import com.emotion.service.ScriptContextService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ScriptContextServiceImpl implements ScriptContextService {
|
||||
|
||||
@Autowired
|
||||
private SocialProfileInsightMapper socialProfileInsightMapper;
|
||||
|
||||
@Override
|
||||
public String buildSocialInsightContext(String userId, Boolean useSocialInsights) {
|
||||
if (Boolean.FALSE.equals(useSocialInsights) || !StringUtils.hasText(userId)) {
|
||||
return "";
|
||||
}
|
||||
List<SocialProfileInsight> insights = listConfirmedInsights(userId, 8);
|
||||
if (insights.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("【用户确认的人生素材画像】\n");
|
||||
for (SocialProfileInsight insight : insights) {
|
||||
sb.append("- ")
|
||||
.append(insight.getInsightType()).append(":")
|
||||
.append(insight.getLabel());
|
||||
if (StringUtils.hasText(insight.getSummary())) {
|
||||
sb.append("。").append(insight.getSummary());
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("请仅将这些画像作为用户已确认的创作偏好和人生素材,不要编造未确认的社交平台事实。");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getConfirmedInsightLabels(String userId) {
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
return List.of();
|
||||
}
|
||||
return listConfirmedInsights(userId, 8).stream()
|
||||
.map(SocialProfileInsight::getLabel)
|
||||
.filter(StringUtils::hasText)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<SocialProfileInsight> listConfirmedInsights(String userId, int limit) {
|
||||
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
||||
.eq(SocialProfileInsight::getIsDeleted, 0)
|
||||
.eq(SocialProfileInsight::getStatus, "confirmed")
|
||||
.orderByDesc(SocialProfileInsight::getConfirmedAt)
|
||||
.orderByDesc(SocialProfileInsight::getUpdateTime)
|
||||
.last("LIMIT " + Math.max(1, limit));
|
||||
return socialProfileInsightMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.dto.request.social.SocialContentLinkImportRequest;
|
||||
import com.emotion.dto.request.social.SocialContentManualImportRequest;
|
||||
import com.emotion.dto.response.social.SocialContentItemResponse;
|
||||
import com.emotion.entity.SocialContentItem;
|
||||
import com.emotion.entity.SocialProfileInsight;
|
||||
import com.emotion.entity.UserConsentLog;
|
||||
import com.emotion.mapper.SocialContentItemMapper;
|
||||
import com.emotion.mapper.SocialProfileInsightMapper;
|
||||
import com.emotion.mapper.UserConsentLogMapper;
|
||||
import com.emotion.service.SocialContentService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SocialContentServiceImpl extends ServiceImpl<SocialContentItemMapper, SocialContentItem>
|
||||
implements SocialContentService {
|
||||
|
||||
private static final Set<String> PLATFORMS = Set.of("xiaohongshu", "weibo", "wechat", "other");
|
||||
private static final int MAX_CONTENT_LENGTH = 20000;
|
||||
private static final long MAX_SCREENSHOT_SIZE = 5L * 1024L * 1024L;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Autowired
|
||||
private UserConsentLogMapper userConsentLogMapper;
|
||||
|
||||
@Autowired
|
||||
private SocialProfileInsightMapper socialProfileInsightMapper;
|
||||
|
||||
@Override
|
||||
public SocialContentItemResponse manualImport(String userId, SocialContentManualImportRequest request) {
|
||||
validateUser(userId);
|
||||
String platform = normalizePlatform(request.getPlatform());
|
||||
String content = normalizeContent(request.getContent());
|
||||
SocialContentItem item = findDuplicate(userId, platform, content);
|
||||
if (item == null) {
|
||||
item = new SocialContentItem();
|
||||
item.setUserId(userId);
|
||||
item.setPlatform(platform);
|
||||
item.setSourceType("manual_text");
|
||||
item.setTitle(trimToNull(request.getTitle()));
|
||||
item.setContent(content);
|
||||
item.setImportStatus("parsed");
|
||||
item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0);
|
||||
item.setContentHash(hashContent(content));
|
||||
this.save(item);
|
||||
} else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) {
|
||||
item.setApprovedForAi(1);
|
||||
this.updateById(item);
|
||||
}
|
||||
logConsentIfApproved(userId, platform, item.getApprovedForAi());
|
||||
return convertToResponse(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocialContentItemResponse linkImport(String userId, SocialContentLinkImportRequest request) {
|
||||
validateUser(userId);
|
||||
String platform = normalizePlatform(request.getPlatform());
|
||||
if (!StringUtils.hasText(request.getSourceUrl())) {
|
||||
throw new IllegalArgumentException("来源链接不能为空");
|
||||
}
|
||||
String content = StringUtils.hasText(request.getContent())
|
||||
? normalizeContent(request.getContent())
|
||||
: normalizeContent(request.getSourceUrl());
|
||||
SocialContentItem item = findDuplicate(userId, platform, content);
|
||||
if (item == null) {
|
||||
item = new SocialContentItem();
|
||||
item.setUserId(userId);
|
||||
item.setPlatform(platform);
|
||||
item.setSourceType("public_link");
|
||||
item.setSourceUrl(request.getSourceUrl().trim());
|
||||
item.setTitle(trimToNull(request.getTitle()));
|
||||
item.setContent(content);
|
||||
item.setImportStatus("parsed");
|
||||
item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0);
|
||||
item.setContentHash(hashContent(content));
|
||||
this.save(item);
|
||||
} else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) {
|
||||
item.setApprovedForAi(1);
|
||||
this.updateById(item);
|
||||
}
|
||||
logConsentIfApproved(userId, platform, item.getApprovedForAi());
|
||||
return convertToResponse(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocialContentItemResponse screenshotImport(String userId, String platform, MultipartFile file) {
|
||||
validateUser(userId);
|
||||
normalizePlatform(platform);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("截图文件不能为空");
|
||||
}
|
||||
if (file.getSize() > MAX_SCREENSHOT_SIZE) {
|
||||
throw new IllegalArgumentException("截图不能超过5MB");
|
||||
}
|
||||
String name = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(Locale.ROOT);
|
||||
if (!(name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png") || name.endsWith(".webp"))) {
|
||||
throw new IllegalArgumentException("仅支持jpg、png、webp截图");
|
||||
}
|
||||
throw new IllegalStateException("OCR暂未启用,请先使用粘贴文本导入");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SocialContentItemResponse> listByUser(String userId) {
|
||||
validateUser(userId);
|
||||
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialContentItem::getUserId, userId)
|
||||
.eq(SocialContentItem::getIsDeleted, 0)
|
||||
.orderByDesc(SocialContentItem::getCreateTime);
|
||||
return this.list(wrapper).stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteByUser(String userId, String id, Boolean keepConfirmedInsights) {
|
||||
validateUser(userId);
|
||||
SocialContentItem item = getOwned(userId, id);
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
item.setIsDeleted(1);
|
||||
item.setDeletedAt(LocalDateTime.now());
|
||||
this.updateById(item);
|
||||
|
||||
LambdaUpdateWrapper<SocialProfileInsight> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
||||
.eq(SocialProfileInsight::getSourceItemId, id)
|
||||
.eq(SocialProfileInsight::getIsDeleted, 0);
|
||||
if (Boolean.TRUE.equals(keepConfirmedInsights)) {
|
||||
wrapper.ne(SocialProfileInsight::getStatus, "confirmed");
|
||||
}
|
||||
wrapper.set(SocialProfileInsight::getStatus, "deleted")
|
||||
.set(SocialProfileInsight::getIsDeleted, 1)
|
||||
.set(SocialProfileInsight::getDeletedAt, LocalDateTime.now());
|
||||
socialProfileInsightMapper.update(null, wrapper);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocialContentItemResponse updateApproval(String userId, String id, Boolean approvedForAi) {
|
||||
validateUser(userId);
|
||||
SocialContentItem item = getOwned(userId, id);
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
item.setApprovedForAi(Boolean.TRUE.equals(approvedForAi) ? 1 : 0);
|
||||
this.updateById(item);
|
||||
logConsentIfApproved(userId, item.getPlatform(), item.getApprovedForAi());
|
||||
return convertToResponse(item);
|
||||
}
|
||||
|
||||
private SocialContentItem getOwned(String userId, String id) {
|
||||
if (!StringUtils.hasText(id)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialContentItem::getId, id)
|
||||
.eq(SocialContentItem::getUserId, userId)
|
||||
.eq(SocialContentItem::getIsDeleted, 0);
|
||||
return this.getOne(wrapper, false);
|
||||
}
|
||||
|
||||
private SocialContentItem findDuplicate(String userId, String platform, String content) {
|
||||
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialContentItem::getUserId, userId)
|
||||
.eq(SocialContentItem::getPlatform, platform)
|
||||
.eq(SocialContentItem::getContentHash, hashContent(content))
|
||||
.eq(SocialContentItem::getIsDeleted, 0);
|
||||
return this.getOne(wrapper, false);
|
||||
}
|
||||
|
||||
private void logConsentIfApproved(String userId, String platform, Integer approvedForAi) {
|
||||
if (approvedForAi == null || approvedForAi != 1) {
|
||||
return;
|
||||
}
|
||||
UserConsentLog log = new UserConsentLog();
|
||||
log.setUserId(userId);
|
||||
log.setPlatform(platform);
|
||||
log.setConsentType("ai_profile_analysis");
|
||||
log.setConsentVersion("v1");
|
||||
log.setScope("imported_social_content");
|
||||
log.setPurpose("用于生成可编辑的人生画像,并增强人生剧本生成");
|
||||
log.setStatus("granted");
|
||||
log.setGrantedAt(LocalDateTime.now());
|
||||
userConsentLogMapper.insert(log);
|
||||
}
|
||||
|
||||
private String normalizePlatform(String platform) {
|
||||
String value = platform == null ? "" : platform.trim().toLowerCase(Locale.ROOT);
|
||||
if (!PLATFORMS.contains(value)) {
|
||||
throw new IllegalArgumentException("不支持的平台");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String normalizeContent(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new IllegalArgumentException("导入内容不能为空");
|
||||
}
|
||||
String normalized = content.replaceAll("\\s+", " ").trim();
|
||||
if (normalized.length() > MAX_CONTENT_LENGTH) {
|
||||
throw new IllegalArgumentException("导入内容不能超过20000个字符");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String hashContent(String content) {
|
||||
return DigestUtils.md5DigestAsHex(content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private boolean isApproved(SocialContentItem item) {
|
||||
return item.getApprovedForAi() != null && item.getApprovedForAi() == 1;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
private void validateUser(String userId) {
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
throw new IllegalArgumentException("用户未登录");
|
||||
}
|
||||
}
|
||||
|
||||
private SocialContentItemResponse convertToResponse(SocialContentItem item) {
|
||||
SocialContentItemResponse response = new SocialContentItemResponse();
|
||||
BeanUtils.copyProperties(item, response);
|
||||
response.setApprovedForAi(isApproved(item));
|
||||
response.setSourceDeleted(item.getIsDeleted() != null && item.getIsDeleted() == 1);
|
||||
if (item.getCreateTime() != null) {
|
||||
response.setCreateTime(item.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (item.getUpdateTime() != null) {
|
||||
response.setUpdateTime(item.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.dto.request.social.SocialInsightGenerateRequest;
|
||||
import com.emotion.dto.request.social.SocialInsightUpdateRequest;
|
||||
import com.emotion.dto.response.social.SocialProfileInsightResponse;
|
||||
import com.emotion.entity.SocialContentItem;
|
||||
import com.emotion.entity.SocialProfileInsight;
|
||||
import com.emotion.mapper.SocialContentItemMapper;
|
||||
import com.emotion.mapper.SocialProfileInsightMapper;
|
||||
import com.emotion.service.SocialInsightService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SocialInsightServiceImpl extends ServiceImpl<SocialProfileInsightMapper, SocialProfileInsight>
|
||||
implements SocialInsightService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final Set<String> STATUS_VALUES = Set.of("suggested", "confirmed", "rejected", "deleted");
|
||||
|
||||
@Autowired
|
||||
private SocialContentItemMapper contentItemMapper;
|
||||
|
||||
@Override
|
||||
public List<SocialProfileInsightResponse> generateInsights(String userId, SocialInsightGenerateRequest request) {
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialContentItem::getUserId, userId)
|
||||
.eq(SocialContentItem::getIsDeleted, 0)
|
||||
.eq(SocialContentItem::getApprovedForAi, 1)
|
||||
.isNotNull(SocialContentItem::getContent)
|
||||
.orderByDesc(SocialContentItem::getCreateTime);
|
||||
if (request != null && request.getSourceItemIds() != null && !request.getSourceItemIds().isEmpty()) {
|
||||
wrapper.in(SocialContentItem::getId, request.getSourceItemIds());
|
||||
}
|
||||
|
||||
List<SocialContentItem> contentItems = contentItemMapper.selectList(wrapper);
|
||||
List<SocialProfileInsight> saved = new ArrayList<>();
|
||||
for (SocialContentItem item : contentItems) {
|
||||
for (SocialProfileInsight insight : extractInsights(userId, item)) {
|
||||
if (existsActiveInsight(userId, item.getId(), insight.getInsightType(), insight.getLabel())) {
|
||||
continue;
|
||||
}
|
||||
this.save(insight);
|
||||
saved.add(insight);
|
||||
}
|
||||
}
|
||||
|
||||
return saved.stream()
|
||||
.sorted(Comparator.comparing(SocialProfileInsight::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SocialProfileInsightResponse> listByUser(String userId, String status) {
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
return List.of();
|
||||
}
|
||||
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
||||
.eq(SocialProfileInsight::getIsDeleted, 0);
|
||||
if (StringUtils.hasText(status)) {
|
||||
wrapper.eq(SocialProfileInsight::getStatus, status);
|
||||
}
|
||||
wrapper.orderByDesc(SocialProfileInsight::getUpdateTime)
|
||||
.orderByDesc(SocialProfileInsight::getCreateTime);
|
||||
return this.list(wrapper).stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocialProfileInsightResponse updateByUser(String userId, String id, SocialInsightUpdateRequest request) {
|
||||
SocialProfileInsight insight = getOwnedInsight(userId, id);
|
||||
if (insight == null || request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(request.getLabel())) {
|
||||
insight.setLabel(request.getLabel().trim());
|
||||
insight.setUserEdited(1);
|
||||
}
|
||||
if (request.getSummary() != null) {
|
||||
insight.setSummary(request.getSummary().trim());
|
||||
insight.setUserEdited(1);
|
||||
}
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
String nextStatus = request.getStatus().trim().toLowerCase(Locale.ROOT);
|
||||
if (!STATUS_VALUES.contains(nextStatus)) {
|
||||
throw new IllegalArgumentException("不支持的画像状态");
|
||||
}
|
||||
insight.setStatus(nextStatus);
|
||||
if ("confirmed".equals(nextStatus)) {
|
||||
insight.setConfirmedAt(LocalDateTime.now());
|
||||
}
|
||||
if ("deleted".equals(nextStatus)) {
|
||||
insight.setIsDeleted(1);
|
||||
insight.setDeletedAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
this.updateById(insight);
|
||||
return convertToResponse(insight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteByUser(String userId, String id) {
|
||||
SocialProfileInsight insight = getOwnedInsight(userId, id);
|
||||
if (insight == null) {
|
||||
return false;
|
||||
}
|
||||
insight.setStatus("deleted");
|
||||
insight.setIsDeleted(1);
|
||||
insight.setDeletedAt(LocalDateTime.now());
|
||||
return this.updateById(insight);
|
||||
}
|
||||
|
||||
private List<SocialProfileInsight> extractInsights(String userId, SocialContentItem item) {
|
||||
String content = item.getContent() == null ? "" : item.getContent().trim();
|
||||
if (content.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<SocialProfileInsight> insights = new ArrayList<>();
|
||||
addIfMatched(insights, userId, item, "value", "职场成长", "用户内容中多次出现工作、职场或创业相关表达,适合转化为事业成长线。",
|
||||
content, BigDecimal.valueOf(0.78), "工作", "职场", "老板", "产品", "创业", "公司", "项目");
|
||||
addIfMatched(insights, userId, item, "value", "被认可", "用户对被看见、被肯定或证明自己有较强叙事诉求。",
|
||||
content, BigDecimal.valueOf(0.74), "认可", "看见", "夸", "肯定", "证明", "成绩", "价值");
|
||||
addIfMatched(insights, userId, item, "interest", "旅行探索", "用户表达了对远方、城市或旅行场景的兴趣,可用于开放式人生分支。",
|
||||
content, BigDecimal.valueOf(0.70), "旅行", "旅游", "城市", "远方", "大理", "海边", "出发");
|
||||
addIfMatched(insights, userId, item, "interest", "创作表达", "用户内容中有写作、音乐、设计或内容创作倾向,适合作为主角天赋线。",
|
||||
content, BigDecimal.valueOf(0.72), "写作", "音乐", "创作", "设计", "内容", "画", "摄影");
|
||||
addIfMatched(insights, userId, item, "emotion_pattern", "关系修复", "用户对关系、告别或重新理解自己有明显表达,可用于情感修复线。",
|
||||
content, BigDecimal.valueOf(0.69), "关系", "分手", "告别", "喜欢", "爱", "朋友", "家人");
|
||||
addIfMatched(insights, userId, item, "script_theme", "低谷逆袭", "用户提到压力、失败或低谷,可转化为爽文开端和反转动力。",
|
||||
content, BigDecimal.valueOf(0.73), "低谷", "失败", "焦虑", "难过", "压力", "崩溃", "迷茫");
|
||||
|
||||
if (insights.isEmpty()) {
|
||||
insights.add(buildInsight(userId, item, "script_theme", "自我成长",
|
||||
"这段内容适合作为自我探索和人生选择的素材。",
|
||||
excerpt(content), BigDecimal.valueOf(0.58)));
|
||||
}
|
||||
return insights;
|
||||
}
|
||||
|
||||
private void addIfMatched(List<SocialProfileInsight> insights, String userId, SocialContentItem item,
|
||||
String type, String label, String summary, String content,
|
||||
BigDecimal confidence, String... keywords) {
|
||||
String lower = content.toLowerCase(Locale.ROOT);
|
||||
for (String keyword : keywords) {
|
||||
if (lower.contains(keyword.toLowerCase(Locale.ROOT))) {
|
||||
insights.add(buildInsight(userId, item, type, label, summary, excerptAround(content, keyword), confidence));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SocialProfileInsight buildInsight(String userId, SocialContentItem item, String type, String label,
|
||||
String summary, String evidence, BigDecimal confidence) {
|
||||
SocialProfileInsight insight = new SocialProfileInsight();
|
||||
insight.setUserId(userId);
|
||||
insight.setSourceItemId(item.getId());
|
||||
insight.setInsightType(type);
|
||||
insight.setLabel(label);
|
||||
insight.setSummary(summary);
|
||||
insight.setEvidenceExcerpt(evidence);
|
||||
insight.setConfidence(confidence);
|
||||
insight.setStatus("suggested");
|
||||
insight.setUserEdited(0);
|
||||
insight.setIsDeleted(0);
|
||||
return insight;
|
||||
}
|
||||
|
||||
private boolean existsActiveInsight(String userId, String sourceItemId, String type, String label) {
|
||||
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
||||
.eq(SocialProfileInsight::getSourceItemId, sourceItemId)
|
||||
.eq(SocialProfileInsight::getInsightType, type)
|
||||
.eq(SocialProfileInsight::getLabel, label)
|
||||
.eq(SocialProfileInsight::getIsDeleted, 0);
|
||||
return this.count(wrapper) > 0;
|
||||
}
|
||||
|
||||
private SocialProfileInsight getOwnedInsight(String userId, String id) {
|
||||
if (!StringUtils.hasText(userId) || !StringUtils.hasText(id)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
||||
.eq(SocialProfileInsight::getId, id)
|
||||
.eq(SocialProfileInsight::getIsDeleted, 0);
|
||||
return this.getOne(wrapper, false);
|
||||
}
|
||||
|
||||
private String excerptAround(String content, String keyword) {
|
||||
int index = content.indexOf(keyword);
|
||||
if (index < 0) {
|
||||
return excerpt(content);
|
||||
}
|
||||
int start = Math.max(0, index - 45);
|
||||
int end = Math.min(content.length(), index + keyword.length() + 65);
|
||||
return excerpt(content.substring(start, end));
|
||||
}
|
||||
|
||||
private String excerpt(String text) {
|
||||
String normalized = text.replaceAll("\\s+", " ").trim();
|
||||
if (normalized.length() <= 120) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, 120) + "...";
|
||||
}
|
||||
|
||||
private SocialProfileInsightResponse convertToResponse(SocialProfileInsight insight) {
|
||||
SocialProfileInsightResponse response = new SocialProfileInsightResponse();
|
||||
BeanUtils.copyProperties(insight, response);
|
||||
response.setId(insight.getId());
|
||||
response.setUserEdited(insight.getUserEdited() != null && insight.getUserEdited() == 1);
|
||||
response.setSourceDeleted(isSourceDeleted(insight.getSourceItemId()));
|
||||
if (insight.getCreateTime() != null) {
|
||||
response.setCreateTime(insight.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (insight.getUpdateTime() != null) {
|
||||
response.setUpdateTime(insight.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private Boolean isSourceDeleted(String sourceItemId) {
|
||||
if (!StringUtils.hasText(sourceItemId)) {
|
||||
return true;
|
||||
}
|
||||
SocialContentItem item = contentItemMapper.selectById(sourceItemId);
|
||||
return item == null || (item.getIsDeleted() != null && item.getIsDeleted() == 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.dto.request.SystemConfigUpdateRequest;
|
||||
import com.emotion.dto.response.LoginConfigResponse;
|
||||
import com.emotion.dto.response.SystemConfigResponse;
|
||||
import com.emotion.entity.SystemConfig;
|
||||
import com.emotion.mapper.SystemConfigMapper;
|
||||
import com.emotion.service.SystemConfigService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 系统配置服务实现类
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-27
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SystemConfigServiceImpl extends ServiceImpl<SystemConfigMapper, SystemConfig> implements SystemConfigService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String CACHE_PREFIX = "system_config:";
|
||||
private static final long CACHE_EXPIRE_HOURS = 24;
|
||||
private static final String SMS_LOGIN_ENABLED_KEY = "sms_login_enabled";
|
||||
|
||||
@Override
|
||||
public List<SystemConfigResponse> getVisibleConfigList() {
|
||||
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SystemConfig::getIsVisible, 1)
|
||||
.orderByAsc(SystemConfig::getSortOrder);
|
||||
List<SystemConfig> configs = list(wrapper);
|
||||
return configs.stream().map(this::convertToResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchUpdateConfigs(List<SystemConfigUpdateRequest> requests) {
|
||||
for (SystemConfigUpdateRequest request : requests) {
|
||||
LambdaUpdateWrapper<SystemConfig> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(SystemConfig::getConfigKey, request.getConfigKey())
|
||||
.set(SystemConfig::getConfigValue, request.getConfigValue());
|
||||
update(updateWrapper);
|
||||
|
||||
// 清除缓存
|
||||
redisTemplate.delete(CACHE_PREFIX + request.getConfigKey());
|
||||
log.info("更新系统配置: key={}, value={}", request.getConfigKey(), request.getConfigValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigValue(String configKey) {
|
||||
// 先查 Redis 缓存
|
||||
String cacheKey = CACHE_PREFIX + configKey;
|
||||
String cached = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 查数据库
|
||||
LambdaQueryWrapper<SystemConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SystemConfig::getConfigKey, configKey);
|
||||
SystemConfig config = getOne(wrapper);
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
redisTemplate.opsForValue().set(cacheKey, config.getConfigValue(), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||
return config.getConfigValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginConfigResponse getLoginConfig() {
|
||||
LoginConfigResponse response = new LoginConfigResponse();
|
||||
response.setWechatLoginEnabled(true);
|
||||
|
||||
String smsEnabled = getConfigValue(SMS_LOGIN_ENABLED_KEY);
|
||||
response.setSmsLoginEnabled("true".equalsIgnoreCase(smsEnabled));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体转响应对象
|
||||
*/
|
||||
private SystemConfigResponse convertToResponse(SystemConfig config) {
|
||||
SystemConfigResponse response = new SystemConfigResponse();
|
||||
response.setConfigKey(config.getConfigKey());
|
||||
response.setConfigValue(config.getConfigValue());
|
||||
response.setValueType(config.getValueType());
|
||||
response.setConfigGroup(config.getConfigGroup());
|
||||
response.setConfigName(config.getConfigName());
|
||||
response.setDescription(config.getDescription());
|
||||
response.setSortOrder(config.getSortOrder());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
import com.emotion.exception.TokenException;
|
||||
import com.emotion.service.AuthService;
|
||||
import com.emotion.service.TokenService;
|
||||
import com.emotion.util.TokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 令牌服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class TokenServiceImpl implements TokenService {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Autowired
|
||||
private TokenUtil tokenUtil;
|
||||
|
||||
@Override
|
||||
public UserInfoResponse getUserInfoByToken(HttpServletRequest request) {
|
||||
String userId = validateTokenAndGetUserId(request);
|
||||
return authService.getCurrentUserInfo(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsernameByToken(HttpServletRequest request) {
|
||||
String token = tokenUtil.extractToken(request);
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
throw new TokenException("未提供访问令牌");
|
||||
}
|
||||
|
||||
if (!authService.validateToken(token)) {
|
||||
throw new TokenException("访问令牌无效或已过期");
|
||||
}
|
||||
|
||||
String username = authService.getUsernameFromToken(token);
|
||||
if (username == null) {
|
||||
throw new TokenException("访问令牌无效");
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateTokenAndGetUserId(HttpServletRequest request) {
|
||||
String token = tokenUtil.extractToken(request);
|
||||
if (!tokenUtil.isValidToken(token)) {
|
||||
throw new TokenException("未提供访问令牌");
|
||||
}
|
||||
|
||||
if (!authService.validateToken(token)) {
|
||||
throw new TokenException("访问令牌无效或已过期");
|
||||
}
|
||||
|
||||
String userId = authService.getUserIdFromToken(token);
|
||||
if (userId == null) {
|
||||
throw new TokenException("访问令牌无效");
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.TopicInteractionCreateRequest;
|
||||
import com.emotion.dto.request.TopicInteractionPageRequest;
|
||||
import com.emotion.dto.request.TopicInteractionUpdateRequest;
|
||||
import com.emotion.dto.response.TopicInteractionResponse;
|
||||
import com.emotion.entity.TopicInteraction;
|
||||
import com.emotion.mapper.TopicInteractionMapper;
|
||||
import com.emotion.service.TopicInteractionService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 话题互动服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class TopicInteractionServiceImpl extends ServiceImpl<TopicInteractionMapper, TopicInteraction> implements TopicInteractionService {
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public PageResult<TopicInteractionResponse> getPageWithResponse(TopicInteractionPageRequest request) {
|
||||
Page<TopicInteraction> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<TopicInteraction> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 根据关键词查询
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(TopicInteraction::getContent, request.getKeyword())
|
||||
.or().like(TopicInteraction::getUserInput, request.getKeyword())
|
||||
.or().like(TopicInteraction::getAiResponse, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 根据话题ID查询
|
||||
if (StringUtils.hasText(request.getTopicId())) {
|
||||
wrapper.eq(TopicInteraction::getTopicId, request.getTopicId());
|
||||
}
|
||||
|
||||
// 根据用户ID查询
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(TopicInteraction::getCreateBy, request.getUserId());
|
||||
}
|
||||
|
||||
// 根据互动类型查询
|
||||
if (StringUtils.hasText(request.getInteractionType())) {
|
||||
wrapper.eq(TopicInteraction::getType, request.getInteractionType());
|
||||
}
|
||||
|
||||
wrapper.eq(TopicInteraction::getIsDeleted, 0).orderByDesc(TopicInteraction::getCreateTime);
|
||||
Page<TopicInteraction> resultPage = this.page(page, wrapper);
|
||||
|
||||
// 转换为响应对象
|
||||
PageResult<TopicInteractionResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.toList());
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopicInteractionResponse getTopicInteractionResponseById(String id) {
|
||||
TopicInteraction topicInteraction = this.getById(id);
|
||||
if (topicInteraction == null || topicInteraction.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(topicInteraction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopicInteractionResponse createTopicInteractionWithResponse(TopicInteractionCreateRequest request) {
|
||||
TopicInteraction topicInteraction = new TopicInteraction();
|
||||
BeanUtils.copyProperties(request, topicInteraction);
|
||||
// 设置用户ID为当前登录用户
|
||||
topicInteraction.setCreateBy(getCurrentUserId());
|
||||
this.save(topicInteraction);
|
||||
return convertToResponse(topicInteraction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopicInteractionResponse updateTopicInteractionWithResponse(TopicInteractionUpdateRequest request) {
|
||||
TopicInteraction topicInteraction = this.getById(request.getId());
|
||||
if (topicInteraction == null || topicInteraction.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getTopicId())) {
|
||||
topicInteraction.setTopicId(request.getTopicId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getInteractionType())) {
|
||||
topicInteraction.setType(request.getInteractionType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getContent())) {
|
||||
topicInteraction.setContent(request.getContent());
|
||||
}
|
||||
if (StringUtils.hasText(request.getUserInput())) {
|
||||
topicInteraction.setUserInput(request.getUserInput());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAiResponse())) {
|
||||
topicInteraction.setAiResponse(request.getAiResponse());
|
||||
}
|
||||
if (request.getRating() != null) {
|
||||
topicInteraction.setRating(request.getRating());
|
||||
}
|
||||
if (StringUtils.hasText(request.getFeedback())) {
|
||||
topicInteraction.setFeedback(request.getFeedback());
|
||||
}
|
||||
|
||||
this.updateById(topicInteraction);
|
||||
return convertToResponse(topicInteraction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteTopicInteraction(String id) {
|
||||
TopicInteraction topicInteraction = this.getById(id);
|
||||
if (topicInteraction == null) {
|
||||
return false;
|
||||
}
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体对象转换为响应对象
|
||||
*/
|
||||
private TopicInteractionResponse convertToResponse(TopicInteraction topicInteraction) {
|
||||
if (topicInteraction == null) {
|
||||
return null;
|
||||
}
|
||||
TopicInteractionResponse response = new TopicInteractionResponse();
|
||||
BeanUtils.copyProperties(topicInteraction, response);
|
||||
response.setId(topicInteraction.getId());
|
||||
if (topicInteraction.getCreateTime() != null) {
|
||||
response.setCreateTime(topicInteraction.getCreateTime().format(FORMATTER));
|
||||
}
|
||||
if (topicInteraction.getUpdateTime() != null) {
|
||||
response.setUpdateTime(topicInteraction.getUpdateTime().format(FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID(模拟实现)
|
||||
*/
|
||||
private String getCurrentUserId() {
|
||||
// 实际项目中应从token中获取用户ID
|
||||
return "current_user_id";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.dto.request.tts.TtsTaskCreateRequest;
|
||||
import com.emotion.dto.response.tts.TtsTaskResponse;
|
||||
import com.emotion.entity.EpicScript;
|
||||
import com.emotion.entity.TtsTask;
|
||||
import com.emotion.mapper.EpicScriptMapper;
|
||||
import com.emotion.mapper.TtsTaskMapper;
|
||||
import com.emotion.service.TtsEngineClient;
|
||||
import com.emotion.service.TtsTaskService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> implements TtsTaskService {
|
||||
|
||||
private static final String SOURCE_TYPE_EPIC_SCRIPT = "epic_script";
|
||||
private static final String STATUS_PENDING = "pending";
|
||||
private static final String STATUS_PROCESSING = "processing";
|
||||
private static final String STATUS_SUCCESS = "success";
|
||||
private static final String STATUS_FAILED = "failed";
|
||||
private static final double FALLBACK_SPEECH_RATE = 0.92D;
|
||||
private static final double FALLBACK_PITCH = 0D;
|
||||
private static final String FALLBACK_EMOTION = "story";
|
||||
private static final int NATURAL_PARAGRAPH_LIMIT = 140;
|
||||
|
||||
private final EpicScriptMapper epicScriptMapper;
|
||||
private final TtsEngineClient ttsEngineClient;
|
||||
private final Executor taskExecutor;
|
||||
|
||||
@Value("${emotion.tts.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${emotion.tts.output-dir:/data/uploads/emotion-museum/tts}")
|
||||
private String outputDir;
|
||||
|
||||
@Value("${emotion.tts.public-url-prefix:/uploads/emotion-museum/tts}")
|
||||
private String publicUrlPrefix;
|
||||
|
||||
@Value("${emotion.tts.max-text-length:5000}")
|
||||
private int maxTextLength;
|
||||
|
||||
@Value("${emotion.tts.default-voice:default_zh_female}")
|
||||
private String defaultVoice;
|
||||
|
||||
@Value("${emotion.tts.default-speech-rate:0.92}")
|
||||
private double defaultSpeechRate;
|
||||
|
||||
@Value("${emotion.tts.default-pitch:0}")
|
||||
private double defaultPitch;
|
||||
|
||||
@Value("${emotion.tts.default-emotion:story}")
|
||||
private String defaultEmotion;
|
||||
|
||||
public TtsTaskServiceImpl(EpicScriptMapper epicScriptMapper,
|
||||
TtsEngineClient ttsEngineClient,
|
||||
@Qualifier("taskExecutor") Executor taskExecutor) {
|
||||
this.epicScriptMapper = epicScriptMapper;
|
||||
this.ttsEngineClient = ttsEngineClient;
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TtsTaskResponse createOrReuse(TtsTaskCreateRequest request) {
|
||||
if (!enabled) {
|
||||
throw new IllegalStateException("TTS service is disabled");
|
||||
}
|
||||
|
||||
String userId = currentUserId();
|
||||
return createOrReuseForUser(userId, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prewarmEpicScript(String userId, String sourceId) {
|
||||
if (!enabled || !StringUtils.hasText(userId) || !StringUtils.hasText(sourceId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TtsTaskCreateRequest request = new TtsTaskCreateRequest();
|
||||
request.setSourceType(SOURCE_TYPE_EPIC_SCRIPT);
|
||||
request.setSourceId(sourceId);
|
||||
createOrReuseForUser(userId, request);
|
||||
} catch (Exception e) {
|
||||
log.warn("TTS prewarm skipped, userId={}, sourceId={}, error={}", userId, sourceId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private TtsTaskResponse createOrReuseForUser(String userId, TtsTaskCreateRequest request) {
|
||||
String sourceType = normalizeSourceType(request.getSourceType());
|
||||
String sourceId = request.getSourceId().trim();
|
||||
String voice = resolveVoice(request.getVoice());
|
||||
TtsEngineClient.SynthesisOptions options = resolveOptions(request);
|
||||
String cleaned = cleanText(loadSourceText(userId, sourceType, sourceId));
|
||||
if (!StringUtils.hasText(cleaned)) {
|
||||
throw new IllegalArgumentException("Source text is empty");
|
||||
}
|
||||
if (cleaned.length() > maxTextLength) {
|
||||
cleaned = limitReadableText(cleaned, maxTextLength);
|
||||
}
|
||||
|
||||
String hash = DigestUtils.md5DigestAsHex((voice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
|
||||
TtsTask owned = findOwnedTask(userId, sourceType, sourceId, voice, hash);
|
||||
if (owned != null) {
|
||||
if (STATUS_FAILED.equals(owned.getStatus()) && !recoverGeneratedAudio(owned)) {
|
||||
log.info("Retrying failed TTS task, oldTaskId={}, sourceType={}, sourceId={}",
|
||||
owned.getId(), sourceType, sourceId);
|
||||
} else {
|
||||
incrementRequestCount(owned);
|
||||
return toResponse(owned);
|
||||
}
|
||||
}
|
||||
|
||||
TtsTask cachedSuccess = findSuccessfulCache(voice, hash);
|
||||
if (cachedSuccess != null && StringUtils.hasText(cachedSuccess.getAudioUrl())) {
|
||||
TtsTask task = buildTask(userId, sourceType, sourceId, voice, hash, cleaned.length());
|
||||
task.setStatus(STATUS_SUCCESS);
|
||||
task.setAudioPath(cachedSuccess.getAudioPath());
|
||||
task.setAudioUrl(cachedSuccess.getAudioUrl());
|
||||
task.setDurationMs(cachedSuccess.getDurationMs());
|
||||
save(task);
|
||||
incrementRequestCount(cachedSuccess);
|
||||
return toResponse(task);
|
||||
}
|
||||
|
||||
TtsTask task = buildTask(userId, sourceType, sourceId, voice, hash, cleaned.length());
|
||||
save(task);
|
||||
String synthesisText = cleaned;
|
||||
CompletableFuture.runAsync(() -> process(task.getId(), synthesisText, voice, task.getAudioPath(), options), taskExecutor);
|
||||
return toResponse(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TtsTaskResponse getTask(String id) {
|
||||
String userId = currentUserId();
|
||||
TtsTask task = getById(id);
|
||||
if (task == null || !userId.equals(task.getUserId())) {
|
||||
return null;
|
||||
}
|
||||
if (STATUS_FAILED.equals(task.getStatus())) {
|
||||
recoverGeneratedAudio(task);
|
||||
}
|
||||
return toResponse(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TtsTaskResponse getBySource(String sourceType, String sourceId, String voice,
|
||||
Double speechRate, Double pitch, String emotion) {
|
||||
String userId = currentUserId();
|
||||
String normalizedSourceType = normalizeSourceType(sourceType);
|
||||
String normalizedVoice = resolveVoice(voice);
|
||||
TtsEngineClient.SynthesisOptions options = resolveOptions(speechRate, pitch, emotion);
|
||||
String cleaned = cleanText(loadSourceText(userId, normalizedSourceType, sourceId));
|
||||
if (cleaned.length() > maxTextLength) {
|
||||
cleaned = limitReadableText(cleaned, maxTextLength);
|
||||
}
|
||||
String hash = DigestUtils.md5DigestAsHex((normalizedVoice + "\n" + options.cacheKey() + "\n" + cleaned).getBytes(StandardCharsets.UTF_8));
|
||||
TtsTask task = findOwnedTask(userId, normalizedSourceType, sourceId, normalizedVoice, hash);
|
||||
if (task != null && STATUS_FAILED.equals(task.getStatus())) {
|
||||
recoverGeneratedAudio(task);
|
||||
}
|
||||
return task == null ? null : toResponse(task);
|
||||
}
|
||||
|
||||
private boolean recoverGeneratedAudio(TtsTask task) {
|
||||
if (task == null || !StringUtils.hasText(task.getAudioPath())) {
|
||||
return false;
|
||||
}
|
||||
File file = new File(task.getAudioPath());
|
||||
if (!file.isFile() || file.length() <= 0L) {
|
||||
return false;
|
||||
}
|
||||
task.setStatus(STATUS_SUCCESS);
|
||||
task.setErrorMessage(null);
|
||||
updateById(task);
|
||||
log.info("Recovered generated TTS audio, taskId={}, audioPath={}", task.getId(), task.getAudioPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void process(String taskId, String text, String voice, String outputPath, TtsEngineClient.SynthesisOptions options) {
|
||||
try {
|
||||
TtsTask task = getById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
task.setStatus(STATUS_PROCESSING);
|
||||
task.setErrorMessage(null);
|
||||
updateById(task);
|
||||
|
||||
TtsEngineClient.TtsEngineResult result = ttsEngineClient.synthesize(text, voice, outputPath, options);
|
||||
task = getById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
if (result.isSuccess()) {
|
||||
task.setStatus(STATUS_SUCCESS);
|
||||
task.setDurationMs(result.getDurationMs());
|
||||
if (StringUtils.hasText(result.getAudioPath())) {
|
||||
task.setAudioPath(result.getAudioPath());
|
||||
}
|
||||
task.setErrorMessage(null);
|
||||
} else {
|
||||
task.setStatus(STATUS_FAILED);
|
||||
task.setErrorMessage(limitError(result.getErrorMessage()));
|
||||
}
|
||||
updateById(task);
|
||||
} catch (Exception e) {
|
||||
log.warn("TTS task processing failed, taskId={}", taskId, e);
|
||||
TtsTask task = getById(taskId);
|
||||
if (task != null) {
|
||||
if (recoverGeneratedAudio(task)) {
|
||||
return;
|
||||
}
|
||||
task.setStatus(STATUS_FAILED);
|
||||
task.setErrorMessage(limitError(e.getMessage()));
|
||||
updateById(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TtsTask buildTask(String userId, String sourceType, String sourceId, String voice, String hash, int textLength) {
|
||||
String filename = hash + ".wav";
|
||||
return TtsTask.builder()
|
||||
.userId(userId)
|
||||
.sourceType(sourceType)
|
||||
.sourceId(sourceId)
|
||||
.textHash(hash)
|
||||
.textLength(textLength)
|
||||
.voice(voice)
|
||||
.status(STATUS_PENDING)
|
||||
.audioPath(joinPath(outputDir, filename))
|
||||
.audioUrl(joinPath(publicUrlPrefix, filename))
|
||||
.requestCount(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TtsTask findOwnedTask(String userId, String sourceType, String sourceId, String voice, String hash) {
|
||||
return getOne(new LambdaQueryWrapper<TtsTask>()
|
||||
.eq(TtsTask::getUserId, userId)
|
||||
.eq(TtsTask::getSourceType, sourceType)
|
||||
.eq(TtsTask::getSourceId, sourceId)
|
||||
.eq(TtsTask::getVoice, voice)
|
||||
.eq(TtsTask::getTextHash, hash)
|
||||
.eq(TtsTask::getIsDeleted, 0)
|
||||
.orderByDesc(TtsTask::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private TtsTask findSuccessfulCache(String voice, String hash) {
|
||||
return getOne(new LambdaQueryWrapper<TtsTask>()
|
||||
.eq(TtsTask::getTextHash, hash)
|
||||
.eq(TtsTask::getVoice, voice)
|
||||
.eq(TtsTask::getStatus, STATUS_SUCCESS)
|
||||
.eq(TtsTask::getIsDeleted, 0)
|
||||
.orderByDesc(TtsTask::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private void incrementRequestCount(TtsTask task) {
|
||||
task.setRequestCount((task.getRequestCount() == null ? 0 : task.getRequestCount()) + 1);
|
||||
updateById(task);
|
||||
}
|
||||
|
||||
private String loadSourceText(String userId, String sourceType, String sourceId) {
|
||||
if (!SOURCE_TYPE_EPIC_SCRIPT.equals(sourceType)) {
|
||||
throw new IllegalArgumentException("Unsupported sourceType");
|
||||
}
|
||||
EpicScript script = epicScriptMapper.selectById(sourceId);
|
||||
if (script == null || !userId.equals(script.getUserId())) {
|
||||
throw new IllegalArgumentException("Script not found");
|
||||
}
|
||||
|
||||
StringBuilder text = new StringBuilder();
|
||||
append(text, script.getTitle());
|
||||
Map<String, Object> plotJson = script.getPlotJson();
|
||||
Object fullContent = plotJson == null ? null : plotJson.get("fullContent");
|
||||
if (fullContent != null && StringUtils.hasText(String.valueOf(fullContent))) {
|
||||
append(text, String.valueOf(fullContent));
|
||||
} else {
|
||||
append(text, script.getPlotIntro());
|
||||
append(text, script.getPlotTurning());
|
||||
append(text, script.getPlotClimax());
|
||||
append(text, script.getPlotEnding());
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
public static String cleanText(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
String normalized = text.replace("\r\n", "\n")
|
||||
.replace('\r', '\n')
|
||||
.replaceAll("!\\[[^\\]]*]\\([^)]*\\)", "")
|
||||
.replaceAll("\\[([^\\]]+)]\\([^)]*\\)", "$1")
|
||||
.replaceAll("(?m)^\\s{0,3}#{1,6}\\s*", "")
|
||||
.replaceAll("(?m)^\\s*>\\s?", "")
|
||||
.replaceAll("(?m)^\\s*[-*+]\\s+", "")
|
||||
.replaceAll("(?m)^\\s*\\d+[.)、]\\s+", "")
|
||||
.replaceAll("<[^>]+>", "")
|
||||
.replaceAll("[*_`~]", "")
|
||||
.replaceAll("[“”]", "\"")
|
||||
.replaceAll("[‘’]", "'")
|
||||
.replaceAll("\\.{3,}", "……")
|
||||
.replaceAll("-{2,}", ",")
|
||||
.replaceAll("[\\t\\u00A0]+", " ")
|
||||
.replaceAll(" {2,}", " ")
|
||||
.replaceAll("(?<=[\\p{IsHan}])[ \\t\\u00A0]+(?=[\\p{IsHan}])", "")
|
||||
.replaceAll("[ \\t\\u00A0]*([,。!?;:、,.!?;:])[ \\t\\u00A0]*", "$1")
|
||||
.replaceAll(",", ",")
|
||||
.replaceAll("!", "!")
|
||||
.replaceAll("\\?", "?")
|
||||
.replaceAll(";", ";")
|
||||
.replaceAll(":", ":");
|
||||
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
for (String paragraph : normalized.split("\\n+")) {
|
||||
String trimmed = paragraph.trim();
|
||||
if (!StringUtils.hasText(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
paragraphs.addAll(toReadableParagraphs(trimmed));
|
||||
}
|
||||
return String.join("\n\n", paragraphs).trim();
|
||||
}
|
||||
|
||||
private TtsTaskResponse toResponse(TtsTask task) {
|
||||
return TtsTaskResponse.builder()
|
||||
.id(task.getId())
|
||||
.sourceType(task.getSourceType())
|
||||
.sourceId(task.getSourceId())
|
||||
.status(task.getStatus())
|
||||
.voice(task.getVoice())
|
||||
.audioUrl(STATUS_SUCCESS.equals(task.getStatus()) ? task.getAudioUrl() : null)
|
||||
.durationMs(task.getDurationMs())
|
||||
.errorMessage(task.getErrorMessage())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TtsEngineClient.SynthesisOptions resolveOptions(TtsTaskCreateRequest request) {
|
||||
return resolveOptions(request.getSpeechRate(), request.getPitch(), request.getEmotion());
|
||||
}
|
||||
|
||||
private TtsEngineClient.SynthesisOptions resolveOptions(Double requestSpeechRate, Double requestPitch, String requestEmotion) {
|
||||
double speechRate = requestSpeechRate == null ? defaultOrFallback(defaultSpeechRate, FALLBACK_SPEECH_RATE) : requestSpeechRate;
|
||||
double pitch = requestPitch == null ? defaultPitch : requestPitch;
|
||||
String emotion = StringUtils.hasText(requestEmotion)
|
||||
? requestEmotion.trim()
|
||||
: (StringUtils.hasText(defaultEmotion) ? defaultEmotion.trim() : FALLBACK_EMOTION);
|
||||
return new TtsEngineClient.SynthesisOptions(
|
||||
round(clamp(speechRate, 0.60D, 1.40D)),
|
||||
round(clamp(pitch, -20D, 20D)),
|
||||
emotion.toLowerCase(Locale.ROOT)
|
||||
);
|
||||
}
|
||||
|
||||
private String currentUserId() {
|
||||
String userId = UserContextHolder.getCurrentUserId();
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
throw new IllegalArgumentException("User not logged in");
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
private String normalizeSourceType(String sourceType) {
|
||||
return StringUtils.hasText(sourceType) ? sourceType.trim() : SOURCE_TYPE_EPIC_SCRIPT;
|
||||
}
|
||||
|
||||
private String resolveVoice(String voice) {
|
||||
return StringUtils.hasText(voice) ? voice.trim() : defaultVoice;
|
||||
}
|
||||
|
||||
private static void append(StringBuilder text, String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
text.append(value).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> toReadableParagraphs(String paragraph) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (int index = 0; index < paragraph.length(); index++) {
|
||||
char ch = paragraph.charAt(index);
|
||||
current.append(ch);
|
||||
if (isHardSentenceEnd(ch) || (current.length() >= NATURAL_PARAGRAPH_LIMIT && isSoftPause(ch))) {
|
||||
addReadablePart(parts, current);
|
||||
}
|
||||
}
|
||||
addReadablePart(parts, current);
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static void addReadablePart(List<String> parts, StringBuilder current) {
|
||||
String value = current.toString().trim();
|
||||
current.setLength(0);
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return;
|
||||
}
|
||||
if (value.length() > NATURAL_PARAGRAPH_LIMIT + 40) {
|
||||
splitLongText(parts, value);
|
||||
return;
|
||||
}
|
||||
parts.add(ensureSentenceEnding(value));
|
||||
}
|
||||
|
||||
private static void splitLongText(List<String> parts, String value) {
|
||||
StringBuilder chunk = new StringBuilder();
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
char ch = value.charAt(index);
|
||||
chunk.append(ch);
|
||||
if (chunk.length() >= NATURAL_PARAGRAPH_LIMIT) {
|
||||
parts.add(ensureSentenceEnding(chunk.toString().trim()));
|
||||
chunk.setLength(0);
|
||||
}
|
||||
}
|
||||
if (chunk.length() > 0) {
|
||||
parts.add(ensureSentenceEnding(chunk.toString().trim()));
|
||||
}
|
||||
}
|
||||
|
||||
private static String ensureSentenceEnding(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return "";
|
||||
}
|
||||
char last = value.charAt(value.length() - 1);
|
||||
return isHardSentenceEnd(last) || isSoftPause(last) ? value : value + "。";
|
||||
}
|
||||
|
||||
private static boolean isHardSentenceEnd(char ch) {
|
||||
return ch == '。' || ch == '!' || ch == '?' || ch == ';' || ch == '…';
|
||||
}
|
||||
|
||||
private static boolean isSoftPause(char ch) {
|
||||
return ch == ',' || ch == '、' || ch == ':';
|
||||
}
|
||||
|
||||
private static String limitReadableText(String text, int limit) {
|
||||
if (text.length() <= limit) {
|
||||
return text;
|
||||
}
|
||||
String truncated = text.substring(0, limit);
|
||||
int cut = Math.max(
|
||||
Math.max(truncated.lastIndexOf('。'), truncated.lastIndexOf('!')),
|
||||
Math.max(truncated.lastIndexOf('?'), truncated.lastIndexOf('\n'))
|
||||
);
|
||||
if (cut > limit * 0.75) {
|
||||
return truncated.substring(0, cut + 1).trim();
|
||||
}
|
||||
return ensureSentenceEnding(truncated.trim());
|
||||
}
|
||||
|
||||
private static double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private static double round(double value) {
|
||||
return Math.round(value * 100D) / 100D;
|
||||
}
|
||||
|
||||
private static double defaultOrFallback(double value, double fallback) {
|
||||
return value <= 0D ? fallback : value;
|
||||
}
|
||||
|
||||
private static String joinPath(String prefix, String filename) {
|
||||
if (prefix.endsWith("/")) {
|
||||
return prefix + filename;
|
||||
}
|
||||
return prefix + "/" + filename;
|
||||
}
|
||||
|
||||
private static String limitError(String message) {
|
||||
if (message == null) {
|
||||
return "TTS synthesis failed";
|
||||
}
|
||||
return message.length() > 1000 ? message.substring(0, 1000) : message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.copier.CopyOptions;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.userprofile.UserProfileCreateRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.response.userprofile.UserProfileResponse;
|
||||
import com.emotion.entity.LifeEvent;
|
||||
import com.emotion.entity.User;
|
||||
import com.emotion.entity.UserProfile;
|
||||
import com.emotion.mapper.UserProfileMapper;
|
||||
import com.emotion.service.LifeEventService;
|
||||
import com.emotion.service.UserProfileService;
|
||||
import com.emotion.service.UserService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户档案服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-21
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserProfile> implements UserProfileService {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private LifeEventService lifeEventService;
|
||||
|
||||
@Override
|
||||
public UserProfileResponse createProfile(UserProfileCreateRequest request) {
|
||||
log.info("Creating user profile: {}", request);
|
||||
|
||||
// 获取当前登录用户ID
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!StringUtils.hasText(currentUserId)) {
|
||||
throw new RuntimeException("用户未登录");
|
||||
}
|
||||
|
||||
// 检查是否已存在档案
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(UserProfile::getUserId, currentUserId);
|
||||
if (count(queryWrapper) > 0) {
|
||||
throw new RuntimeException("当前用户已存在档案信息");
|
||||
}
|
||||
|
||||
UserProfile userProfile = new UserProfile();
|
||||
BeanUtils.copyProperties(request, userProfile);
|
||||
userProfile.setUserId(currentUserId);
|
||||
userProfile.setStatus(1); // 默认正常
|
||||
|
||||
save(userProfile);
|
||||
log.info("User profile created with ID: {}", userProfile.getId());
|
||||
|
||||
return convertToResponse(userProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileResponse updateProfile(UserProfileUpdateRequest request) {
|
||||
log.info("Updating user profile: {}", request);
|
||||
|
||||
// 获取当前登录用户ID
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!StringUtils.hasText(currentUserId)) {
|
||||
throw new RuntimeException("用户未登录");
|
||||
}
|
||||
|
||||
UserProfile userProfile = null;
|
||||
|
||||
// 先根据请求ID查询
|
||||
if (StringUtils.hasText(request.getId())) {
|
||||
userProfile = getById(request.getId());
|
||||
}
|
||||
|
||||
// 如果没有查到,根据当前用户ID查询
|
||||
if (userProfile == null) {
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(UserProfile::getUserId, currentUserId);
|
||||
userProfile = getOne(queryWrapper);
|
||||
}
|
||||
|
||||
// 如果还是没有,则创建新档案
|
||||
if (userProfile == null) {
|
||||
log.info("User profile not found, creating new profile for user: {}", currentUserId);
|
||||
UserProfileCreateRequest createRequest = new UserProfileCreateRequest();
|
||||
BeanUtils.copyProperties(request, createRequest);
|
||||
return createProfile(createRequest);
|
||||
}
|
||||
|
||||
// 权限校验:只能修改自己的档案
|
||||
if (!userProfile.getUserId().equals(currentUserId)) {
|
||||
throw new RuntimeException("无权修改他人档案");
|
||||
}
|
||||
|
||||
// 使用Hutool的BeanUtil进行部分更新,忽略null值
|
||||
BeanUtil.copyProperties(request, userProfile, CopyOptions.create().setIgnoreNullValue(true));
|
||||
updateById(userProfile);
|
||||
log.info("User profile updated: {}", userProfile.getId());
|
||||
|
||||
return convertToResponse(userProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileResponse getCurrentUserProfile() {
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (!StringUtils.hasText(currentUserId)) {
|
||||
throw new RuntimeException("用户未登录");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(UserProfile::getUserId, currentUserId);
|
||||
// 获取最新的一条(理论上应该只有一条)
|
||||
List<UserProfile> list = list(queryWrapper);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(list.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileResponse getProfileById(String id) {
|
||||
UserProfile userProfile = getById(id);
|
||||
if (userProfile == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(userProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<UserProfileResponse> getProfilePage(UserProfilePageRequest request) {
|
||||
Page<UserProfile> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 构建查询条件
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
queryWrapper.eq(UserProfile::getUserId, request.getUserId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getNickname())) {
|
||||
queryWrapper.like(UserProfile::getNickname, request.getNickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.getMbti())) {
|
||||
queryWrapper.eq(UserProfile::getMbti, request.getMbti());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
queryWrapper.eq(UserProfile::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc(UserProfile::getCreateTime);
|
||||
|
||||
IPage<UserProfile> userProfilePage = page(page, queryWrapper);
|
||||
|
||||
List<UserProfileResponse> list = userProfilePage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<UserProfileResponse> result = new PageResult<>();
|
||||
result.setCurrent(userProfilePage.getCurrent());
|
||||
result.setSize(userProfilePage.getSize());
|
||||
result.setTotal(userProfilePage.getTotal());
|
||||
result.setPages(userProfilePage.getPages());
|
||||
result.setRecords(list);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserProfileResponse> getProfileList(UserProfilePageRequest request) {
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 构建查询条件
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
queryWrapper.eq(UserProfile::getUserId, request.getUserId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getNickname())) {
|
||||
queryWrapper.like(UserProfile::getNickname, request.getNickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.getMbti())) {
|
||||
queryWrapper.eq(UserProfile::getMbti, request.getMbti());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
queryWrapper.eq(UserProfile::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc(UserProfile::getCreateTime);
|
||||
|
||||
List<UserProfile> list = list(queryWrapper);
|
||||
return list.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteProfile(String id) {
|
||||
log.info("Deleting user profile: {}", id);
|
||||
return removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int migrateLifeEventsFromProfiles() {
|
||||
log.info("开始迁移用户档案中的生命事件数据到t_life_event表");
|
||||
|
||||
// 查询所有有生命事件数据的用户档案
|
||||
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.and(w -> w
|
||||
.isNotNull(UserProfile::getChildhoodContent).ne(UserProfile::getChildhoodContent, "")
|
||||
.or().isNotNull(UserProfile::getPeakContent).ne(UserProfile::getPeakContent, "")
|
||||
.or().isNotNull(UserProfile::getValleyContent).ne(UserProfile::getValleyContent, "")
|
||||
);
|
||||
|
||||
List<UserProfile> profiles = list(queryWrapper);
|
||||
log.info("找到 {} 个有生命事件数据的用户档案", profiles.size());
|
||||
|
||||
int migratedCount = 0;
|
||||
|
||||
for (UserProfile profile : profiles) {
|
||||
String userId = profile.getUserId();
|
||||
List<LifeEvent> eventsToSave = new ArrayList<>();
|
||||
|
||||
// 迁移童年记忆
|
||||
if (StringUtils.hasText(profile.getChildhoodContent())) {
|
||||
if (!isEventExists(userId, "childhood")) {
|
||||
LifeEvent childhoodEvent = createLifeEventFromProfile(
|
||||
userId,
|
||||
"童年记忆",
|
||||
profile.getChildhoodContent(),
|
||||
profile.getChildhoodDate(),
|
||||
"childhood"
|
||||
);
|
||||
eventsToSave.add(childhoodEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移高光时刻 (peak -> joy)
|
||||
if (StringUtils.hasText(profile.getPeakContent())) {
|
||||
if (!isEventExists(userId, "joy")) {
|
||||
LifeEvent peakEvent = createLifeEventFromProfile(
|
||||
userId,
|
||||
"光芒闪耀的时刻",
|
||||
profile.getPeakContent(),
|
||||
profile.getPeakDate(),
|
||||
"joy"
|
||||
);
|
||||
eventsToSave.add(peakEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移低谷时期 (valley -> low)
|
||||
if (StringUtils.hasText(profile.getValleyContent())) {
|
||||
if (!isEventExists(userId, "low")) {
|
||||
LifeEvent valleyEvent = createLifeEventFromProfile(
|
||||
userId,
|
||||
"在暗夜中潜行",
|
||||
profile.getValleyContent(),
|
||||
profile.getValleyDate(),
|
||||
"low"
|
||||
);
|
||||
eventsToSave.add(valleyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
if (!eventsToSave.isEmpty()) {
|
||||
lifeEventService.saveBatch(eventsToSave);
|
||||
migratedCount += eventsToSave.size();
|
||||
log.info("用户 {} 迁移了 {} 条生命事件", userId, eventsToSave.size());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("生命事件数据迁移完成,共迁移 {} 条记录", migratedCount);
|
||||
return migratedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已存在指定标签的生命事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tag 标签
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean isEventExists(String userId, String tag) {
|
||||
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LifeEvent::getUserId, userId)
|
||||
.eq(LifeEvent::getIsDeleted, 0)
|
||||
.like(LifeEvent::getTags, tag);
|
||||
return lifeEventService.count(wrapper) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户档案数据创建生命事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param title 事件标题
|
||||
* @param content 事件内容
|
||||
* @param eventDate 事件日期
|
||||
* @param tag 事件标签
|
||||
* @return 生命事件实体
|
||||
*/
|
||||
private LifeEvent createLifeEventFromProfile(String userId, String title, String content,
|
||||
LocalDate eventDate, String tag) {
|
||||
LifeEvent event = new LifeEvent();
|
||||
event.setUserId(userId);
|
||||
event.setTitle(title);
|
||||
event.setContent(content);
|
||||
event.setEventType("milestone");
|
||||
event.setTags(List.of(tag));
|
||||
|
||||
// 设置事件日期,如果没有则使用当前时间
|
||||
if (eventDate != null) {
|
||||
event.setEventDate(eventDate.atStartOfDay());
|
||||
} else {
|
||||
event.setEventDate(java.time.LocalDateTime.now());
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
private UserProfileResponse convertToResponse(UserProfile userProfile) {
|
||||
if (userProfile == null) {
|
||||
return null;
|
||||
}
|
||||
UserProfileResponse response = new UserProfileResponse();
|
||||
BeanUtils.copyProperties(userProfile, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.UserCreateRequest;
|
||||
import com.emotion.dto.request.UserPageRequest;
|
||||
import com.emotion.dto.request.UserUpdateRequest;
|
||||
import com.emotion.dto.request.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.response.UserResponse;
|
||||
import com.emotion.entity.User;
|
||||
import com.emotion.mapper.UserMapper;
|
||||
import com.emotion.service.UserService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public UserServiceImpl(PasswordEncoder passwordEncoder) {
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<UserResponse> getPageWithResponse(UserPageRequest request) {
|
||||
Page<User> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(User::getUsername, request.getKeyword())
|
||||
.or().like(User::getNickname, request.getKeyword())
|
||||
.or().like(User::getEmail, request.getKeyword()));
|
||||
}
|
||||
|
||||
// 账号查询
|
||||
if (StringUtils.hasText(request.getAccount())) {
|
||||
wrapper.eq(User::getAccount, request.getAccount());
|
||||
}
|
||||
|
||||
// 用户名查询
|
||||
if (StringUtils.hasText(request.getUsername())) {
|
||||
wrapper.like(User::getUsername, request.getUsername());
|
||||
}
|
||||
|
||||
// 邮箱查询
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
wrapper.eq(User::getEmail, request.getEmail());
|
||||
}
|
||||
|
||||
// 手机号查询
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
wrapper.eq(User::getPhone, request.getPhone());
|
||||
}
|
||||
|
||||
// 状态查询
|
||||
if (request.getStatus() != null) {
|
||||
wrapper.eq(User::getStatus, request.getStatus());
|
||||
}
|
||||
|
||||
// 会员等级查询
|
||||
if (StringUtils.hasText(request.getMemberLevel())) {
|
||||
wrapper.eq(User::getMemberLevel, request.getMemberLevel());
|
||||
}
|
||||
|
||||
wrapper.eq(User::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(User::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(User::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(User::getCreateTime);
|
||||
}
|
||||
|
||||
Page<User> resultPage = this.page(page, wrapper);
|
||||
List<UserResponse> userResponses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<UserResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(userResponses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserResponse getUserResponseById(String id) {
|
||||
User user = this.getById(id);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserResponse createUserWithResponse(UserCreateRequest request) {
|
||||
User user = new User();
|
||||
user.setAccount(request.getAccount());
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setEmail(request.getEmail());
|
||||
user.setPhone(request.getPhone());
|
||||
user.setNickname(request.getUsername());
|
||||
user.setMemberLevel("free");
|
||||
user.setStatus(1);
|
||||
user.setIsVerified(0);
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
|
||||
this.save(user);
|
||||
return convertToResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserResponse updateUserWithResponse(UserUpdateRequest request) {
|
||||
User user = this.getById(request.getId());
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getUsername())) {
|
||||
user.setUsername(request.getUsername());
|
||||
}
|
||||
if (StringUtils.hasText(request.getNickname())) {
|
||||
user.setNickname(request.getNickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
user.setPhone(request.getPhone());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAvatar())) {
|
||||
user.setAvatar(request.getAvatar());
|
||||
}
|
||||
if (request.getBirthDate() != null) {
|
||||
user.setBirthDate(request.getBirthDate());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLocation())) {
|
||||
user.setLocation(request.getLocation());
|
||||
}
|
||||
if (StringUtils.hasText(request.getBio())) {
|
||||
user.setBio(request.getBio());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
user.setStatus(request.getStatus());
|
||||
}
|
||||
if (StringUtils.hasText(request.getMemberLevel())) {
|
||||
user.setMemberLevel(request.getMemberLevel());
|
||||
}
|
||||
|
||||
this.updateById(user);
|
||||
return convertToResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserResponse updateCurrentUserProfileWithResponse(UserProfileUpdateRequest request) {
|
||||
// 从UserContextHolder获取当前用户ID
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
User user = this.getById(currentUserId);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只更新非空字段
|
||||
if (StringUtils.hasText(request.getNickname())) {
|
||||
user.setNickname(request.getNickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.getEmail())) {
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (StringUtils.hasText(request.getPhone())) {
|
||||
user.setPhone(request.getPhone());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAvatar())) {
|
||||
user.setAvatar(request.getAvatar());
|
||||
}
|
||||
if (request.getBirthDate() != null) {
|
||||
user.setBirthDate(request.getBirthDate());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLocation())) {
|
||||
user.setLocation(request.getLocation());
|
||||
}
|
||||
if (StringUtils.hasText(request.getBio())) {
|
||||
user.setBio(request.getBio());
|
||||
}
|
||||
|
||||
this.updateById(user);
|
||||
return convertToResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserResponse getCurrentUserProfileWithResponse() {
|
||||
// 从UserContextHolder获取当前用户ID
|
||||
String currentUserId = UserContextHolder.getCurrentUserId();
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
User user = this.getById(currentUserId);
|
||||
if (user == null || user.getIsDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
return convertToResponse(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteUser(String id) {
|
||||
User user = this.getById(id);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
|
||||
return this.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByAccount(String account) {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getAccount, account)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByEmail(String email) {
|
||||
if (!StringUtils.hasText(email)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getEmail, email)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByPhone(String phone) {
|
||||
if (!StringUtils.hasText(phone)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getPhone, phone)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
|
||||
if (!StringUtils.hasText(thirdPartyType) || !StringUtils.hasText(thirdPartyId)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getThirdPartyType, thirdPartyType)
|
||||
.eq(User::getThirdPartyId, thirdPartyId)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User createUser(String account, String username, String password, String email, String phone) {
|
||||
User user = new User();
|
||||
user.setAccount(account);
|
||||
user.setUsername(username);
|
||||
user.setNickname(username); // 默认昵称与用户名相同
|
||||
user.setPassword(passwordEncoder.encode(password)); // 加密密码
|
||||
user.setEmail(email);
|
||||
user.setPhone(phone);
|
||||
user.setMemberLevel("free"); // 默认免费会员
|
||||
user.setStatus(1); // 默认启用
|
||||
user.setIsVerified(0); // 默认未验证
|
||||
user.setLastActiveTime(LocalDateTime.now());
|
||||
|
||||
this.save(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLastActiveTime(String userId, LocalDateTime lastActiveTime) {
|
||||
if (!StringUtils.hasText(userId) || lastActiveTime == null) {
|
||||
return;
|
||||
}
|
||||
User user = this.getById(userId);
|
||||
if (user != null && user.getIsDeleted() == 0) {
|
||||
user.setLastActiveTime(lastActiveTime);
|
||||
this.updateById(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private UserResponse convertToResponse(User user) {
|
||||
UserResponse response = new UserResponse();
|
||||
BeanUtils.copyProperties(user, response);
|
||||
response.setId(user.getId());
|
||||
if (user.getCreateTime() != null) {
|
||||
response.setCreateTime(user.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (user.getUpdateTime() != null) {
|
||||
response.setUpdateTime(user.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (user.getLastActiveTime() != null) {
|
||||
response.setLastActiveTime(user.getLastActiveTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.dto.request.UserStatsCreateRequest;
|
||||
import com.emotion.dto.request.UserStatsIncrementRequest;
|
||||
import com.emotion.dto.request.UserStatsPageRequest;
|
||||
import com.emotion.dto.request.UserStatsUpdateValueRequest;
|
||||
import com.emotion.dto.response.UserStatsResponse;
|
||||
import com.emotion.entity.UserStats;
|
||||
import com.emotion.mapper.UserStatsMapper;
|
||||
import com.emotion.service.UserStatsService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户统计服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class UserStatsServiceImpl extends ServiceImpl<UserStatsMapper, UserStats> implements UserStatsService {
|
||||
|
||||
private final SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public UserStatsServiceImpl(SnowflakeIdGenerator snowflakeIdGenerator) {
|
||||
this.snowflakeIdGenerator = snowflakeIdGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<UserStatsResponse> getPageWithResponse(UserStatsPageRequest request) {
|
||||
Page<UserStats> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<UserStats> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(UserStats::getUserId, request.getKeyword())
|
||||
.or().like(UserStats::getStatsType, request.getKeyword());
|
||||
}
|
||||
|
||||
// 用户ID查询
|
||||
if (StringUtils.hasText(request.getUserId())) {
|
||||
wrapper.eq(UserStats::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
// 统计类型查询
|
||||
if (StringUtils.hasText(request.getStatsType())) {
|
||||
wrapper.eq(UserStats::getStatsType, request.getStatsType());
|
||||
}
|
||||
|
||||
wrapper.eq(UserStats::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(UserStats::getCreateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(UserStats::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(UserStats::getCreateTime);
|
||||
}
|
||||
|
||||
Page<UserStats> resultPage = this.page(page, wrapper);
|
||||
List<UserStatsResponse> responses = resultPage.getRecords().stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<UserStatsResponse> pageResult = new PageResult<>();
|
||||
pageResult.setCurrent(resultPage.getCurrent());
|
||||
pageResult.setSize(resultPage.getSize());
|
||||
pageResult.setTotal(resultPage.getTotal());
|
||||
pageResult.setPages(resultPage.getPages());
|
||||
pageResult.setRecords(responses);
|
||||
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatsValue(UserStatsUpdateValueRequest request) {
|
||||
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(UserStats::getUserId, request.getUserId())
|
||||
.eq(UserStats::getStatsType, request.getStatsType())
|
||||
.eq(UserStats::getIsDeleted, 0)
|
||||
.set(UserStats::getValue, request.getValue())
|
||||
.set(UserStats::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementStatsValue(UserStatsIncrementRequest request) {
|
||||
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(UserStats::getUserId, request.getUserId())
|
||||
.eq(UserStats::getStatsType, request.getStatsType())
|
||||
.eq(UserStats::getIsDeleted, 0)
|
||||
.setSql("value = value + " + request.getIncrement())
|
||||
.set(UserStats::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recalculateUserStats(String userId) {
|
||||
// 这里应该根据业务逻辑重新计算用户统计数据
|
||||
// 示例:更新用户的总活跃天数、总发帖数等
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recalculateAllUserStats() {
|
||||
// 这里应该重新计算所有用户的统计数据
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserStatsResponse createOrUpdateUserStatsWithResponse(UserStatsCreateRequest request) {
|
||||
UserStats stats = createOrUpdateUserStats(request.getUserId(), request.getStatsType(), request.getValue(),
|
||||
request.getPeriod());
|
||||
return convertToResponse(stats);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteExpiredStats(Integer days) {
|
||||
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
|
||||
LambdaUpdateWrapper<UserStats> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.lt(UserStats::getCreateTime, expireTime)
|
||||
.set(UserStats::getIsDeleted, 1)
|
||||
.set(UserStats::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新用户统计
|
||||
*/
|
||||
private UserStats createOrUpdateUserStats(String userId, String statsType, Double value, String period) {
|
||||
UserStats stats = getByUserIdAndStatsType(userId, statsType);
|
||||
if (stats == null) {
|
||||
stats = new UserStats();
|
||||
stats.setId(snowflakeIdGenerator.nextIdAsString());
|
||||
stats.setUserId(userId);
|
||||
stats.setStatsType(statsType);
|
||||
stats.setValue(value);
|
||||
stats.setPeriod(period);
|
||||
stats.setCreateTime(LocalDateTime.now());
|
||||
stats.setUpdateTime(LocalDateTime.now());
|
||||
stats.setIsDeleted(0);
|
||||
this.save(stats);
|
||||
} else {
|
||||
stats.setValue(value);
|
||||
stats.setPeriod(period);
|
||||
stats.setUpdateTime(LocalDateTime.now());
|
||||
this.updateById(stats);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID和统计类型获取统计信息
|
||||
*/
|
||||
private UserStats getByUserIdAndStatsType(String userId, String statsType) {
|
||||
LambdaQueryWrapper<UserStats> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(UserStats::getUserId, userId)
|
||||
.eq(UserStats::getStatsType, statsType)
|
||||
.eq(UserStats::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为响应对象
|
||||
*/
|
||||
private UserStatsResponse convertToResponse(UserStats stats) {
|
||||
UserStatsResponse response = new UserStatsResponse();
|
||||
BeanUtils.copyProperties(stats, response);
|
||||
response.setId(stats.getId());
|
||||
if (stats.getCreateTime() != null) {
|
||||
response.setCreateTime(stats.getCreateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
if (stats.getUpdateTime() != null) {
|
||||
response.setUpdateTime(stats.getUpdateTime().format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.request.WebSocketRequest;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.dto.websocket.ChatRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
import com.emotion.dto.websocket.WebSocketMessage;
|
||||
import com.emotion.entity.Message;
|
||||
import com.emotion.entity.Conversation;
|
||||
import com.emotion.service.WebSocketService;
|
||||
import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.MessageService;
|
||||
import com.emotion.service.ConversationService;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* WebSocket服务实现类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-25
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WebSocketServiceImpl implements WebSocketService {
|
||||
|
||||
@Autowired
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private AiRuntimeService aiRuntimeService;
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
// 在线用户管理
|
||||
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*/
|
||||
@Override
|
||||
public void handleChatMessage(WebSocketRequest webSocketRequest, String sessionId, Principal principal) {
|
||||
try {
|
||||
log.info("处理聊天消息: request={}, sessionId={}, principal={}", webSocketRequest, sessionId, principal);
|
||||
|
||||
// 验证请求参数
|
||||
if (webSocketRequest.getContent() == null || webSocketRequest.getContent().trim().isEmpty()) {
|
||||
sendErrorMessage(getUserId(principal, sessionId), "消息内容不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
setWebSocketRequestDefaults(webSocketRequest, principal, sessionId);
|
||||
|
||||
log.info("确定用户身份: userId={}, senderType={}", webSocketRequest.getSenderId(),
|
||||
webSocketRequest.getSenderType());
|
||||
|
||||
// 转换请求对象
|
||||
ChatRequest chatRequest = convertToChatRequest(webSocketRequest);
|
||||
|
||||
// 构建用户消息
|
||||
WebSocketMessage userMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(chatRequest.getConversationId())
|
||||
.type("TEXT")
|
||||
.content(chatRequest.getContent())
|
||||
.senderId(chatRequest.getSenderId())
|
||||
.senderType(getSenderType(chatRequest.getSenderType()))
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 发送用户消息到会话
|
||||
if (chatRequest.getConversationId() != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + chatRequest.getConversationId(), userMessage);
|
||||
}
|
||||
|
||||
// 发送给用户私有队列
|
||||
messagingTemplate.convertAndSendToUser(chatRequest.getSenderId(), "/queue/messages", userMessage);
|
||||
|
||||
// 发送AI思考状态
|
||||
sendAiThinkingMessage(chatRequest.getSenderId(), chatRequest.getConversationId());
|
||||
|
||||
// 异步调用AI服务
|
||||
processAiResponse(chatRequest);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理聊天消息失败", e);
|
||||
sendErrorMessage(getUserId(principal, sessionId), "消息处理失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
*/
|
||||
@Override
|
||||
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
// 设置默认值
|
||||
setConnectRequestDefaults(request, principal, sessionId);
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
|
||||
request.getUserId(), sessionId, isUserAuthenticated(request.getUserId()));
|
||||
|
||||
// 记录在线用户
|
||||
onlineUsers.put(sessionId, request.getUserId());
|
||||
|
||||
// 发送连接成功消息
|
||||
WebSocketMessage connectMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type("CONNECTION")
|
||||
.content("连接成功")
|
||||
.senderId("system")
|
||||
.senderType("SYSTEM")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(request.getUserId(), "/queue/messages", connectMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
*/
|
||||
@Override
|
||||
public void handleUserDisconnect(String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = onlineUsers.remove(sessionId);
|
||||
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户断开连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
*/
|
||||
@Override
|
||||
public void handleHeartbeat(String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = onlineUsers.get(sessionId);
|
||||
if (userId == null && principal != null) {
|
||||
userId = principal.getName();
|
||||
}
|
||||
|
||||
// 发送心跳响应
|
||||
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type("HEARTBEAT")
|
||||
.content("pong")
|
||||
.senderId("system")
|
||||
.senderType("SYSTEM")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
if (userId != null) {
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理心跳消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送AI思考状态消息
|
||||
*/
|
||||
private void sendAiThinkingMessage(String userId, String conversationId) {
|
||||
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type("AI_THINKING")
|
||||
.content("AI正在思考中...")
|
||||
.senderId("ai")
|
||||
.senderType("AI")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
|
||||
|
||||
if (conversationId != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步处理AI响应
|
||||
*/
|
||||
private void processAiResponse(ChatRequest request) {
|
||||
// 使用线程池异步处理AI响应
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String userId = request.getSenderId();
|
||||
String conversationId = request.getConversationId();
|
||||
|
||||
// 如果没有会话ID,创建新会话
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
conversationId = createNewConversation(userId, request);
|
||||
request.setConversationId(conversationId);
|
||||
}
|
||||
|
||||
// 确保会话存在并更新活跃时间
|
||||
ensureConversationExists(conversationId, userId, request);
|
||||
|
||||
// 保存用户消息到数据库
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(conversationId);
|
||||
userMessage.setUserId(userId);
|
||||
userMessage.setCreateBy(userId); // 设置创建人为当前用户
|
||||
userMessage
|
||||
.setUserType("USER".equals(request.getSenderType()) ? "registered" : "guest");
|
||||
userMessage.setContent(request.getContent());
|
||||
userMessage.setType("text");
|
||||
userMessage.setSender("user");
|
||||
userMessage.setCozeRole("user");
|
||||
userMessage.setCozeContentType("text");
|
||||
userMessage = messageService.createMessage(userMessage);
|
||||
|
||||
String aiMessageId = snowflakeIdGenerator.nextIdAsString();
|
||||
String aiReply = streamChatReply(userId, conversationId, userMessage.getId(), aiMessageId, request.getContent());
|
||||
if (StringUtils.hasText(aiReply)) {
|
||||
saveAiMessageOnly(userId, conversationId, aiMessageId, aiReply);
|
||||
}
|
||||
|
||||
// 更新会话的最后活跃时间和消息数量
|
||||
updateConversationActivity(conversationId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI响应处理失败", e);
|
||||
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
*/
|
||||
private String streamChatReply(String userId, String conversationId, String userMessageId, String aiMessageId, String content) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
AiRuntimeRequest runtimeRequest = new AiRuntimeRequest();
|
||||
runtimeRequest.setSceneCode("chat");
|
||||
runtimeRequest.setUserId(userId);
|
||||
runtimeRequest.setRequestId(aiMessageId);
|
||||
|
||||
JSONObject inputs = new JSONObject();
|
||||
inputs.put("input", content);
|
||||
inputs.put("message", content);
|
||||
inputs.put("prompt", content);
|
||||
inputs.put("conversationId", conversationId);
|
||||
inputs.put("userMessageId", userMessageId);
|
||||
runtimeRequest.setInputs(inputs);
|
||||
|
||||
aiRuntimeService.invokeStream(runtimeRequest, event -> {
|
||||
if ("delta".equals(event.getType()) && event.getContent() != null) {
|
||||
output.append(event.getContent());
|
||||
}
|
||||
sendAiStreamEvent(userId, conversationId, aiMessageId, event);
|
||||
});
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private void sendAiStreamEvent(String userId, String conversationId, String aiMessageId, AiStreamEvent event) {
|
||||
String type = switch (event.getType()) {
|
||||
case "start" -> "AI_STREAM_START";
|
||||
case "delta" -> "AI_STREAM_DELTA";
|
||||
case "done" -> "AI_STREAM_DONE";
|
||||
case "error" -> "AI_STREAM_ERROR";
|
||||
default -> "AI_STREAM_EVENT";
|
||||
};
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("eventType", event.getType());
|
||||
data.put("code", event.getCode());
|
||||
data.put("message", event.getMessage());
|
||||
data.put("seq", event.getSeq());
|
||||
data.put("timestamp", event.getTimestamp());
|
||||
data.put("metadata", event.getMetadata());
|
||||
|
||||
String messageContent = event.getContent();
|
||||
if (!StringUtils.hasText(messageContent)) {
|
||||
messageContent = StringUtils.hasText(event.getMessage()) ? event.getMessage() : "";
|
||||
}
|
||||
|
||||
WebSocketMessage wsMessage = WebSocketMessage.builder()
|
||||
.messageId(aiMessageId)
|
||||
.conversationId(conversationId)
|
||||
.type(type)
|
||||
.content(messageContent)
|
||||
.senderId("ai")
|
||||
.senderType("AI")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.data(data)
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", wsMessage);
|
||||
if (conversationId != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, wsMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAiMessageOnly(String userId, String conversationId, String messageId, String content) {
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setId(messageId);
|
||||
aiMessage.setConversationId(conversationId);
|
||||
aiMessage.setUserId(userId);
|
||||
aiMessage.setCreateBy("ai");
|
||||
aiMessage.setContent(content);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("ai");
|
||||
aiMessage.setCozeRole("assistant");
|
||||
aiMessage.setCozeContentType("text");
|
||||
aiMessage.setTimestamp(LocalDateTime.now());
|
||||
messageService.createMessage(aiMessage);
|
||||
}
|
||||
|
||||
private void sendErrorMessage(String userId, String errorContent) {
|
||||
WebSocketMessage errorMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type("ERROR")
|
||||
.content(errorContent)
|
||||
.senderId("system")
|
||||
.senderType("SYSTEM")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*/
|
||||
@Override
|
||||
public int getOnlineUserCount() {
|
||||
return onlineUsers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
private String createNewConversation(String userId, ChatRequest request) {
|
||||
try {
|
||||
String conversationId = "conv_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Conversation conversation = Conversation.builder()
|
||||
.userId(userId)
|
||||
.userType("USER".equals(request.getSenderType()) ? "registered" : "guest")
|
||||
.title("新对话")
|
||||
.type("chat")
|
||||
.conversationStatus("active")
|
||||
.startTime(LocalDateTime.now())
|
||||
.lastActiveTime(LocalDateTime.now())
|
||||
.messageCount(0)
|
||||
.build();
|
||||
|
||||
// 设置ID
|
||||
conversation.setId(conversationId);
|
||||
|
||||
conversationService.save(conversation);
|
||||
log.info("创建新会话: conversationId={}, userId={}", conversationId, userId);
|
||||
|
||||
return conversationId;
|
||||
} catch (Exception e) {
|
||||
log.error("创建新会话失败: userId={}", userId, e);
|
||||
throw new RuntimeException("创建会话失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保会话存在并更新活跃时间
|
||||
*/
|
||||
private void ensureConversationExists(String conversationId, String userId, ChatRequest request) {
|
||||
try {
|
||||
Conversation conversation = conversationService.getById(conversationId);
|
||||
if (conversation == null) {
|
||||
// 如果会话不存在,创建一个
|
||||
conversation = Conversation.builder()
|
||||
.userId(userId)
|
||||
.userType("USER".equals(request.getSenderType()) ? "registered" : "guest")
|
||||
.title("对话")
|
||||
.type("chat")
|
||||
.conversationStatus("active")
|
||||
.startTime(LocalDateTime.now())
|
||||
.lastActiveTime(LocalDateTime.now())
|
||||
.messageCount(0)
|
||||
.build();
|
||||
|
||||
// 设置ID
|
||||
conversation.setId(conversationId);
|
||||
|
||||
conversationService.save(conversation);
|
||||
log.info("创建会话: conversationId={}, userId={}", conversationId, userId);
|
||||
} else {
|
||||
// 更新最后活跃时间
|
||||
conversation.setLastActiveTime(LocalDateTime.now());
|
||||
conversationService.updateById(conversation);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("确保会话存在失败: conversationId={}, userId={}", conversationId, userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话活跃状态
|
||||
*/
|
||||
private void updateConversationActivity(String conversationId) {
|
||||
try {
|
||||
Conversation conversation = conversationService.getById(conversationId);
|
||||
if (conversation != null) {
|
||||
conversation.setLastActiveTime(LocalDateTime.now());
|
||||
conversation.setMessageCount((conversation.getMessageCount() != null ? conversation.getMessageCount() : 0) + 1);
|
||||
conversationService.updateById(conversation);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新会话活跃状态失败: conversationId={}", conversationId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据换行符分割AI回复并按顺序发送多条消息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param conversationId 会话ID
|
||||
* @param aiReply AI回复内容
|
||||
*/
|
||||
private void sendAiReplyInParts(String userId, String conversationId, String aiReply) {
|
||||
try {
|
||||
log.info("开始处理AI回复消息: userId={}, conversationId={}, aiReply长度={}",
|
||||
userId, conversationId, aiReply != null ? aiReply.length() : 0);
|
||||
|
||||
if (aiReply == null || aiReply.trim().isEmpty()) {
|
||||
log.warn("AI回复内容为空,跳过发送");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否需要分割
|
||||
boolean needsSplit = aiReply.contains("\n\n") || aiReply.contains("\n");
|
||||
|
||||
if (!needsSplit) {
|
||||
// 不需要分割,直接发送完整消息
|
||||
log.info("AI回复无换行符,发送完整消息");
|
||||
sendSingleAiMessage(userId, conversationId, aiReply.trim());
|
||||
return;
|
||||
}
|
||||
|
||||
// 需要分割,按换行符分割并发送多条消息
|
||||
log.info("AI回复包含换行符,开始分割发送");
|
||||
String[] replyParts = splitAiReply(aiReply);
|
||||
|
||||
log.info("AI回复分割完成,共{}个部分", replyParts.length);
|
||||
|
||||
// 按顺序发送每个部分
|
||||
int sentCount = 0;
|
||||
for (int i = 0; i < replyParts.length; i++) {
|
||||
String part = replyParts[i].trim();
|
||||
|
||||
// 跳过空白部分
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 发送消息部分
|
||||
sendSingleAiMessage(userId, conversationId, part);
|
||||
sentCount++;
|
||||
|
||||
log.info("发送AI回复部分 {}/{}: 内容长度={}", sentCount, replyParts.length, part.length());
|
||||
|
||||
// 在多个部分之间添加短暂延迟,模拟自然对话节奏
|
||||
if (i < replyParts.length - 1) {
|
||||
try {
|
||||
Thread.sleep(500); // 延迟500毫秒
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("发送AI回复时被中断");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("AI回复发送完成: userId={}, conversationId={}, 实际发送{}条消息",
|
||||
userId, conversationId, sentCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("分割发送AI回复失败: userId={}, conversationId={}", userId, conversationId, e);
|
||||
|
||||
// 发送错误时,尝试发送完整的原始回复
|
||||
try {
|
||||
WebSocketMessage fallbackMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type("TEXT")
|
||||
.content(aiReply)
|
||||
.senderId("ai")
|
||||
.senderType("AI")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", fallbackMessage);
|
||||
|
||||
if (conversationId != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, fallbackMessage);
|
||||
}
|
||||
|
||||
log.info("已发送完整AI回复作为备用方案");
|
||||
|
||||
} catch (Exception fallbackError) {
|
||||
log.error("发送备用AI回复也失败", fallbackError);
|
||||
sendErrorMessage(userId, "AI回复发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单条AI消息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param conversationId 会话ID
|
||||
* @param content 消息内容
|
||||
*/
|
||||
private void sendSingleAiMessage(String userId, String conversationId, String content) {
|
||||
try {
|
||||
// 保存AI消息到数据库
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setId(snowflakeIdGenerator.nextIdAsString());
|
||||
aiMessage.setConversationId(conversationId);
|
||||
aiMessage.setUserId(userId);
|
||||
aiMessage.setCreateBy("ai");
|
||||
aiMessage.setContent(content);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("ai");
|
||||
aiMessage.setCozeRole("assistant");
|
||||
aiMessage.setCozeContentType("text");
|
||||
aiMessage.setTimestamp(LocalDateTime.now());
|
||||
messageService.createMessage(aiMessage);
|
||||
|
||||
log.info("AI消息已保存到数据库: messageId={}, conversationId={}, contentLength={}",
|
||||
aiMessage.getId(), conversationId, content.length());
|
||||
|
||||
// 构建WebSocket消息
|
||||
WebSocketMessage wsMessage = WebSocketMessage.builder()
|
||||
.messageId(aiMessage.getId())
|
||||
.conversationId(conversationId)
|
||||
.type("TEXT")
|
||||
.content(content)
|
||||
.senderId("ai")
|
||||
.senderType("AI")
|
||||
.status("SENT")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 发送给用户私有队列
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", wsMessage);
|
||||
|
||||
// 发送到会话公共频道
|
||||
if (conversationId != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, wsMessage);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送单条AI消息失败: userId={}, conversationId={}", userId, conversationId, e);
|
||||
sendErrorMessage(userId, "消息发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能分割AI回复内容
|
||||
*
|
||||
* @param aiReply AI回复内容
|
||||
* @return 分割后的内容数组
|
||||
*/
|
||||
private String[] splitAiReply(String aiReply) {
|
||||
if (aiReply == null || aiReply.trim().isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// 首先尝试按双换行符分割(段落分割)
|
||||
if (aiReply.contains("\n\n")) {
|
||||
String[] parts = aiReply.split("\n\n");
|
||||
log.debug("按双换行符分割,得到{}个部分", parts.length);
|
||||
return parts;
|
||||
}
|
||||
|
||||
// 如果没有双换行符,按单换行符分割(行分割)
|
||||
if (aiReply.contains("\n")) {
|
||||
String[] parts = aiReply.split("\n");
|
||||
log.debug("按单换行符分割,得到{}个部分", parts.length);
|
||||
return parts;
|
||||
}
|
||||
|
||||
// 如果没有换行符,返回原始内容(这种情况不应该到达这里)
|
||||
log.debug("没有换行符,返回原始内容");
|
||||
return new String[]{aiReply};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket请求的默认值
|
||||
*/
|
||||
private void setWebSocketRequestDefaults(WebSocketRequest request, Principal principal, String sessionId) {
|
||||
// 如果请求中没有发送者ID,尝试从Principal获取
|
||||
if (request.getSenderId() == null && principal != null) {
|
||||
request.setSenderId(principal.getName());
|
||||
}
|
||||
|
||||
// 如果还是没有发送者ID,使用会话ID作为访客ID
|
||||
if (request.getSenderId() == null) {
|
||||
request.setSenderId("guest_" + sessionId);
|
||||
request.setSenderType("GUEST");
|
||||
}
|
||||
|
||||
// 设置时间戳
|
||||
if (request.getTimestamp() == null) {
|
||||
request.setTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接请求的默认值
|
||||
*/
|
||||
private void setConnectRequestDefaults(ConnectRequest request, Principal principal, String sessionId) {
|
||||
// 优先从Principal获取认证用户信息
|
||||
if (principal != null) {
|
||||
request.setUserId(principal.getName());
|
||||
}
|
||||
|
||||
// 如果还没有userId,生成访客ID
|
||||
if (request.getUserId() == null) {
|
||||
request.setUserId("guest_" + sessionId);
|
||||
}
|
||||
|
||||
// 设置连接时间戳
|
||||
if (request.getTimestamp() == null) {
|
||||
request.setTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*/
|
||||
private String getUserId(Principal principal, String sessionId) {
|
||||
if (principal != null) {
|
||||
return principal.getName();
|
||||
}
|
||||
return "guest_" + sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否已认证
|
||||
*/
|
||||
private boolean isUserAuthenticated(String userId) {
|
||||
return userId != null && !userId.startsWith("guest_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发送者类型
|
||||
*/
|
||||
private String getSenderType(String senderType) {
|
||||
if ("USER".equals(senderType)) {
|
||||
return "USER";
|
||||
} else if ("GUEST".equals(senderType)) {
|
||||
return "GUEST";
|
||||
} else if ("AI".equals(senderType)) {
|
||||
return "AI";
|
||||
} else if ("SYSTEM".equals(senderType)) {
|
||||
return "SYSTEM";
|
||||
}
|
||||
return "USER";
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换WebSocketRequest到ChatRequest
|
||||
*
|
||||
* @param webSocketRequest WebSocket请求对象
|
||||
* @return ChatRequest对象
|
||||
*/
|
||||
private ChatRequest convertToChatRequest(WebSocketRequest webSocketRequest) {
|
||||
return ChatRequest.builder()
|
||||
.content(webSocketRequest.getContent())
|
||||
.senderId(webSocketRequest.getSenderId())
|
||||
.senderType(webSocketRequest.getSenderType())
|
||||
.messageType(webSocketRequest.getMessageType())
|
||||
.conversationId(webSocketRequest.getConversationId())
|
||||
.timestamp(webSocketRequest.getTimestamp())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.config.WechatMiniProgramProperties;
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
import com.emotion.exception.BusinessException;
|
||||
import com.emotion.exception.WechatApiException;
|
||||
import com.emotion.service.WechatMiniProgramService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final WechatMiniProgramProperties properties;
|
||||
|
||||
@Override
|
||||
public WechatCodeSessionResponse code2Session(String code) {
|
||||
validateConfigured();
|
||||
|
||||
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/sns/jscode2session")
|
||||
.queryParam("appid", properties.getAppId())
|
||||
.queryParam("secret", properties.getAppSecret())
|
||||
.queryParam("js_code", code)
|
||||
.queryParam("grant_type", "authorization_code")
|
||||
.toUriString();
|
||||
|
||||
ResponseEntity<String> responseEntity;
|
||||
try {
|
||||
responseEntity = restTemplate.getForEntity(url, String.class);
|
||||
} catch (ResourceAccessException e) {
|
||||
log.error("[WeChatAPI] network error, jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "Wechat API network error: " + e.getMessage(), null);
|
||||
} catch (RestClientException e) {
|
||||
log.error("[WeChatAPI] client error, jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "Wechat API client error: " + e.getMessage(), null);
|
||||
}
|
||||
|
||||
String responseBody = responseEntity.getBody();
|
||||
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("[WeChatAPI] non-2xx response, status={}, body={}",
|
||||
responseEntity.getStatusCode(), responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"Wechat API returned non-2xx status: " + responseEntity.getStatusCode(), responseBody);
|
||||
}
|
||||
|
||||
JSONObject result = parseWechatJsonBody(responseEntity);
|
||||
|
||||
int errcode = result.getIntValue("errcode");
|
||||
if (errcode != 0) {
|
||||
String errmsg = result.getString("errmsg");
|
||||
log.error("[WeChatAPI] business error, errcode={}, errmsg={}", errcode, errmsg);
|
||||
throw new WechatApiException(400,
|
||||
"Wechat API business error: errcode=" + errcode + ", errmsg=" + errmsg, responseBody);
|
||||
}
|
||||
|
||||
String openid = result.getString("openid");
|
||||
String sessionKey = result.getString("session_key");
|
||||
if (!StringUtils.hasText(openid) || !StringUtils.hasText(sessionKey)) {
|
||||
log.error("[WeChatAPI] response missing openid or session_key, body={}", responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"Wechat API response missing openid or session_key", responseBody);
|
||||
}
|
||||
|
||||
WechatCodeSessionResponse response = new WechatCodeSessionResponse();
|
||||
response.setOpenid(openid);
|
||||
response.setSessionKey(sessionKey);
|
||||
response.setUnionid(result.getString("unionid"));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JSONObject parseWechatJsonBody(ResponseEntity<String> responseEntity) {
|
||||
String responseBody = responseEntity.getBody();
|
||||
MediaType contentType = responseEntity.getHeaders().getContentType();
|
||||
boolean jsonContentType = contentType != null && MediaType.APPLICATION_JSON.isCompatibleWith(contentType);
|
||||
boolean jsonLikeBody = StringUtils.hasText(responseBody)
|
||||
&& (responseBody.trim().startsWith("{") || responseBody.trim().startsWith("["));
|
||||
|
||||
if (!jsonContentType && !jsonLikeBody) {
|
||||
log.error("[WeChatAPI] non-JSON response, contentType={}, body={}", contentType, responseBody);
|
||||
throw new WechatApiException(502,
|
||||
"Wechat API returned a non-JSON response: " + contentType, responseBody);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSONObject.parseObject(responseBody);
|
||||
} catch (Exception e) {
|
||||
log.error("[WeChatAPI] JSON parse failed, contentType={}, body={}", contentType, responseBody, e);
|
||||
throw new WechatApiException(jsonContentType ? 500 : 502,
|
||||
"Wechat API response JSON parse failed: " + e.getMessage(), responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfigured() {
|
||||
if (!StringUtils.hasText(properties.getAppId()) || !StringUtils.hasText(properties.getAppSecret())) {
|
||||
throw new BusinessException("微信小程序登录未配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user