From 873b8e55da286827726481acea7a9e81b3e9e657 Mon Sep 17 00:00:00 2001 From: peanut_hzm Date: Thu, 24 Jul 2025 07:38:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=20-=20=E6=A0=87=E5=87=86=E5=8C=96Controller?= =?UTF-8?q?=E5=B1=82=E5=92=8CService=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 新功能: - 创建了完整的Service层架构,包含所有业务实体的Service接口和实现类 - 新增8个标准化的Controller类,支持完整的CRUD操作 - 实现了统一的Request/Response模式和分页查询功能 - 创建了认证服务(AuthService)和令牌服务(TokenService) - 添加了Redis配置和认证拦截器 🏗️ 架构优化: - 移除Controller层所有try-catch块,使用全局异常处理机制 - 创建了专门的异常类(AuthException, TokenException, CaptchaException) - 统一了API返回格式,完善了Result类的方法 - 实现了标准的分页查询和参数校验 📦 新增文件: - 8个Controller类: Achievement, Comment, CommunityPost, Conversation, CozeApiCall, EmotionAnalysis, Reward, UserStats - 12个Service接口和对应的实现类 - 标准化的DTO类(Request/Response) - 异常处理类和拦截器 - 测试用例 🔧 重构优化: - 重写了AuthController,移除所有业务逻辑到Service层 - 优化了MessageController,使用标准的Request/Response格式 - 更新了全局异常处理器,支持多种异常类型 - 完善了WebConfig配置,添加认证拦截器 📊 代码统计: - 新增文件: 60+个 - 新增代码行数: 8000+行 - 重构代码行数: 1000+行 - 移除过时接口: 4个 --- .../java/com/emotion/common/PageResult.java | 55 +++ .../main/java/com/emotion/common/Result.java | 28 ++ .../java/com/emotion/config/RedisConfig.java | 40 ++ .../java/com/emotion/config/WebConfig.java | 23 + .../controller/AchievementController.java | 227 ++++++++++ .../emotion/controller/AiChatController.java | 16 +- .../emotion/controller/AuthController.java | 331 ++++---------- .../emotion/controller/CommentController.java | 249 +++++++++++ .../controller/CommunityPostController.java | 289 +++++++++++++ .../controller/ConversationController.java | 283 ++++++++++++ .../controller/CozeApiCallController.java | 307 +++++++++++++ .../controller/EmotionAnalysisController.java | 287 +++++++++++++ .../emotion/controller/MessageController.java | 313 ++++++-------- .../emotion/controller/RewardController.java | 332 ++++++++++++++ .../emotion/controller/UserController.java | 194 +++++---- .../controller/UserStatsController.java | 270 ++++++++++++ .../com/emotion/dto/request/BaseRequest.java | 23 + .../com/emotion/dto/request/IdRequest.java | 23 + .../com/emotion/dto/request/LoginRequest.java | 41 ++ .../com/emotion/dto/request/PageRequest.java | 20 + .../emotion/dto/request/RegisterRequest.java | 68 +++ .../dto/request/UserCreateRequest.java | 53 +++ .../emotion/dto/response/AuthResponse.java | 38 ++ .../emotion/dto/response/BaseResponse.java | 28 ++ .../emotion/dto/response/CaptchaResponse.java | 28 ++ .../dto/response/UserInfoResponse.java | 73 ++++ .../emotion/dto/response/UserResponse.java | 60 +++ .../com/emotion/exception/AuthException.java | 18 + .../emotion/exception/BusinessException.java | 40 ++ .../emotion/exception/CaptchaException.java | 18 + .../exception/GlobalExceptionHandler.java | 165 +++++++ .../com/emotion/exception/TokenException.java | 18 + .../emotion/interceptor/AuthInterceptor.java | 113 +++++ .../java/com/emotion/mapper/RewardMapper.java | Bin 616 -> 0 bytes .../emotion/service/AchievementService.java | 133 ++++++ .../java/com/emotion/service/AuthService.java | 105 +++++ .../com/emotion/service/CommentService.java | 128 ++++++ .../emotion/service/CommunityPostService.java | 159 +++++++ ...llService.java => CozeApiCallService.java} | 13 +- .../service/EmotionAnalysisService.java | 99 +++++ .../emotion/service/EmotionRecordService.java | 109 +++++ .../emotion/service/GrowthTopicService.java | 124 ++++++ .../com/emotion/service/GuestUserService.java | 123 ++++++ .../java/com/emotion/service/IAiService.java | 56 --- .../emotion/service/IConversationService.java | 121 ------ .../com/emotion/service/IMessageService.java | 141 ------ .../emotion/service/LocationPinService.java | 0 .../com/emotion/service/RewardService.java | 144 +++++++ .../com/emotion/service/TokenService.java | 36 ++ .../service/TopicInteractionService.java | 149 +++++++ .../com/emotion/service/UserStatsService.java | 128 ++++++ .../service/impl/AchievementServiceImpl.java | 259 +++++++++++ .../emotion/service/impl/AuthServiceImpl.java | 405 ++++++++++++++++++ .../service/impl/CommentServiceImpl.java | 261 +++++++++++ .../impl/CommunityPostServiceImpl.java | 323 ++++++++++++++ .../service/impl/ConversationServiceImpl.java | 229 ++++++++++ .../service/impl/CozeApiCallServiceImpl.java | 65 ++- .../impl/EmotionAnalysisServiceImpl.java | 218 ++++++++++ .../impl/EmotionRecordServiceImpl.java | 238 ++++++++++ .../service/impl/GrowthTopicServiceImpl.java | 251 +++++++++++ .../service/impl/GuestUserServiceImpl.java | 233 ++++++++++ .../service/impl/LocationPinServiceImpl.java | 50 +++ .../service/impl/RewardServiceImpl.java | 288 +++++++++++++ .../service/impl/TokenServiceImpl.java | 64 +++ .../impl/TopicInteractionServiceImpl.java | 291 +++++++++++++ .../service/impl/UserStatsServiceImpl.java | 265 ++++++++++++ .../controller/AuthControllerTest.java | 190 ++++++++ 67 files changed, 8619 insertions(+), 850 deletions(-) create mode 100644 backend-single/src/main/java/com/emotion/common/PageResult.java create mode 100644 backend-single/src/main/java/com/emotion/config/RedisConfig.java create mode 100644 backend-single/src/main/java/com/emotion/controller/AchievementController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/CommentController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/CommunityPostController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/ConversationController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/RewardController.java create mode 100644 backend-single/src/main/java/com/emotion/controller/UserStatsController.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/BaseRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/IdRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/PageRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/AuthResponse.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/BaseResponse.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/CaptchaResponse.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java create mode 100644 backend-single/src/main/java/com/emotion/dto/response/UserResponse.java create mode 100644 backend-single/src/main/java/com/emotion/exception/AuthException.java create mode 100644 backend-single/src/main/java/com/emotion/exception/BusinessException.java create mode 100644 backend-single/src/main/java/com/emotion/exception/CaptchaException.java create mode 100644 backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java create mode 100644 backend-single/src/main/java/com/emotion/exception/TokenException.java create mode 100644 backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java delete mode 100644 backend-single/src/main/java/com/emotion/mapper/RewardMapper.java create mode 100644 backend-single/src/main/java/com/emotion/service/AchievementService.java create mode 100644 backend-single/src/main/java/com/emotion/service/AuthService.java create mode 100644 backend-single/src/main/java/com/emotion/service/CommentService.java create mode 100644 backend-single/src/main/java/com/emotion/service/CommunityPostService.java rename backend-single/src/main/java/com/emotion/service/{ICozeApiCallService.java => CozeApiCallService.java} (86%) create mode 100644 backend-single/src/main/java/com/emotion/service/EmotionAnalysisService.java create mode 100644 backend-single/src/main/java/com/emotion/service/EmotionRecordService.java create mode 100644 backend-single/src/main/java/com/emotion/service/GrowthTopicService.java create mode 100644 backend-single/src/main/java/com/emotion/service/GuestUserService.java delete mode 100644 backend-single/src/main/java/com/emotion/service/IAiService.java delete mode 100644 backend-single/src/main/java/com/emotion/service/IConversationService.java delete mode 100644 backend-single/src/main/java/com/emotion/service/IMessageService.java create mode 100644 backend-single/src/main/java/com/emotion/service/LocationPinService.java create mode 100644 backend-single/src/main/java/com/emotion/service/RewardService.java create mode 100644 backend-single/src/main/java/com/emotion/service/TokenService.java create mode 100644 backend-single/src/main/java/com/emotion/service/TopicInteractionService.java create mode 100644 backend-single/src/main/java/com/emotion/service/UserStatsService.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/AchievementServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/ConversationServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/EmotionAnalysisServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/EmotionRecordServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/GrowthTopicServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/GuestUserServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/LocationPinServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/TokenServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java create mode 100644 backend-single/src/main/java/com/emotion/service/impl/UserStatsServiceImpl.java create mode 100644 backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java diff --git a/backend-single/src/main/java/com/emotion/common/PageResult.java b/backend-single/src/main/java/com/emotion/common/PageResult.java new file mode 100644 index 0000000..632ebeb --- /dev/null +++ b/backend-single/src/main/java/com/emotion/common/PageResult.java @@ -0,0 +1,55 @@ +package com.emotion.common; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; + +import java.util.List; + +/** + * 分页结果封装 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class PageResult { + + /** + * 当前页码 + */ + private Long current; + + /** + * 每页大小 + */ + private Long size; + + /** + * 总记录数 + */ + private Long total; + + /** + * 总页数 + */ + private Long pages; + + /** + * 数据列表 + */ + private List records; + + public PageResult() {} + + public PageResult(IPage page) { + this.current = page.getCurrent(); + this.size = page.getSize(); + this.total = page.getTotal(); + this.pages = page.getPages(); + this.records = page.getRecords(); + } + + public static PageResult of(IPage page) { + return new PageResult<>(page); + } +} diff --git a/backend-single/src/main/java/com/emotion/common/Result.java b/backend-single/src/main/java/com/emotion/common/Result.java index 878090b..59cd44d 100644 --- a/backend-single/src/main/java/com/emotion/common/Result.java +++ b/backend-single/src/main/java/com/emotion/common/Result.java @@ -95,6 +95,13 @@ public class Result implements Serializable { return new Result<>(401, "未授权", null); } + /** + * 未授权带消息 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + /** * 禁止访问 */ @@ -102,10 +109,31 @@ public class Result implements Serializable { return new Result<>(403, "禁止访问", null); } + /** + * 禁止访问带消息 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } + + /** + * 请求参数错误 + */ + public static Result badRequest(String message) { + return new Result<>(400, message, null); + } + /** * 资源未找到 */ public static Result notFound() { return new Result<>(404, "资源未找到", null); } + + /** + * 资源未找到带消息 + */ + public static Result notFound(String message) { + return new Result<>(404, message, null); + } } diff --git a/backend-single/src/main/java/com/emotion/config/RedisConfig.java b/backend-single/src/main/java/com/emotion/config/RedisConfig.java new file mode 100644 index 0000000..d78fbd2 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/config/RedisConfig.java @@ -0,0 +1,40 @@ +package com.emotion.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Configuration +public class RedisConfig { + + /** + * 配置RedisTemplate + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // 使用String序列化器作为key的序列化器 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + template.setKeySerializer(stringRedisSerializer); + template.setHashKeySerializer(stringRedisSerializer); + + // 使用JSON序列化器作为value的序列化器 + GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); + template.setValueSerializer(jsonRedisSerializer); + template.setHashValueSerializer(jsonRedisSerializer); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/backend-single/src/main/java/com/emotion/config/WebConfig.java b/backend-single/src/main/java/com/emotion/config/WebConfig.java index abab906..4d03194 100644 --- a/backend-single/src/main/java/com/emotion/config/WebConfig.java +++ b/backend-single/src/main/java/com/emotion/config/WebConfig.java @@ -1,5 +1,6 @@ package com.emotion.config; +import com.emotion.interceptor.AuthInterceptor; import com.emotion.interceptor.UserContextInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -17,6 +18,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { + @Autowired + private AuthInterceptor authInterceptor; + @Autowired private UserContextInterceptor userContextInterceptor; @@ -25,6 +29,25 @@ public class WebConfig implements WebMvcConfigurer { */ @Override public void addInterceptors(InterceptorRegistry registry) { + // 认证拦截器 + registry.addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns( + "/auth/**", + "/error", + "/favicon.ico", + "/actuator/**", + "/swagger-ui/**", + "/swagger-resources/**", + "/v2/api-docs", + "/v3/api-docs", + "/webjars/**", + "/doc.html", + "/static/**", + "/public/**" + ); + + // 用户上下文拦截器 registry.addInterceptor(userContextInterceptor) .addPathPatterns("/**") .excludePathPatterns( diff --git a/backend-single/src/main/java/com/emotion/controller/AchievementController.java b/backend-single/src/main/java/com/emotion/controller/AchievementController.java new file mode 100644 index 0000000..56ed052 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/AchievementController.java @@ -0,0 +1,227 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.Achievement; +import com.emotion.service.AchievementService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 成就控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/achievement") +public class AchievementController { + + @Autowired + private AchievementService achievementService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询成就 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = achievementService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取成就 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + Achievement achievement = achievementService.getById(id); + if (achievement == null) { + return Result.notFound("成就不存在"); + } + return Result.success(convertToResponse(achievement)); + } + + /** + * 创建成就 + */ + @PostMapping + public Result create(@RequestBody Achievement achievement) { + boolean saved = achievementService.save(achievement); + if (!saved) { + return Result.error("创建失败"); + } + return Result.success(convertToResponse(achievement)); + } + + /** + * 更新成就 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody Achievement achievement) { + achievement.setId(id); + boolean updated = achievementService.updateById(achievement); + if (!updated) { + return Result.error("更新失败"); + } + Achievement updatedAchievement = achievementService.getById(id); + return Result.success(convertToResponse(updatedAchievement)); + } + + /** + * 删除成就 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = achievementService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据分类查询成就 + */ + @GetMapping("/category/{category}") + public Result> getByCategory(@PathVariable String category) { + List achievements = achievementService.getByCategory(category); + List responses = achievements.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据稀有度查询成就 + */ + @GetMapping("/rarity/{rarity}") + public Result> getByRarity(@PathVariable String rarity) { + List achievements = achievementService.getByRarity(rarity); + List responses = achievements.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询已解锁的成就 + */ + @GetMapping("/unlocked") + public Result> getUnlockedAchievements() { + List achievements = achievementService.getUnlockedAchievements(); + List responses = achievements.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询未解锁的成就 + */ + @GetMapping("/locked") + public Result> getLockedAchievements() { + List achievements = achievementService.getLockedAchievements(); + List responses = achievements.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 统计已解锁成就数量 + */ + @GetMapping("/count/unlocked") + public Result countUnlockedAchievements() { + Long count = achievementService.countUnlockedAchievements(); + return Result.success(count); + } + + /** + * 统计未解锁成就数量 + */ + @GetMapping("/count/locked") + public Result countLockedAchievements() { + Long count = achievementService.countLockedAchievements(); + return Result.success(count); + } + + /** + * 解锁成就 + */ + @PutMapping("/{id}/unlock") + public Result unlockAchievement(@PathVariable String id) { + boolean unlocked = achievementService.unlockAchievement(id, java.time.LocalDateTime.now()); + if (!unlocked) { + return Result.error("解锁失败"); + } + return Result.success(); + } + + /** + * 更新成就进度 + */ + @PutMapping("/{id}/progress") + public Result updateProgress(@PathVariable String id, @RequestParam Double progress) { + boolean updated = achievementService.updateProgress(id, progress); + if (!updated) { + return Result.error("更新进度失败"); + } + return Result.success(); + } + + /** + * 转换为响应对象 + */ + private AchievementResponse convertToResponse(Achievement achievement) { + AchievementResponse response = new AchievementResponse(); + 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)); + } + return response; + } + + /** + * 成就响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class AchievementResponse extends BaseResponse { + private String title; + private String description; + private String category; + private String rarity; + private String conditionType; + private String conditionValue; + private Double progress; + private String unlockedTime; + private Integer isHidden; + private String iconUrl; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/AiChatController.java b/backend-single/src/main/java/com/emotion/controller/AiChatController.java index 5b283c7..842bbe7 100644 --- a/backend-single/src/main/java/com/emotion/controller/AiChatController.java +++ b/backend-single/src/main/java/com/emotion/controller/AiChatController.java @@ -1,9 +1,9 @@ package com.emotion.controller; import com.emotion.common.Result; -import com.emotion.service.IAiService; -import com.emotion.service.IMessageService; -import com.emotion.service.IConversationService; +import com.emotion.service.AiService; +import com.emotion.service.MessageService; +import com.emotion.service.ConversationService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -23,13 +23,13 @@ import java.util.Map; public class AiChatController { @Autowired - private IAiService aiService; - + private AiService aiService; + @Autowired - private IMessageService messageService; - + private MessageService messageService; + @Autowired - private IConversationService conversationService; + private ConversationService conversationService; /** * 发送聊天消息 diff --git a/backend-single/src/main/java/com/emotion/controller/AuthController.java b/backend-single/src/main/java/com/emotion/controller/AuthController.java index e53cd40..7e55c64 100644 --- a/backend-single/src/main/java/com/emotion/controller/AuthController.java +++ b/backend-single/src/main/java/com/emotion/controller/AuthController.java @@ -1,293 +1,130 @@ package com.emotion.controller; import com.emotion.common.Result; -import com.emotion.entity.User; -import com.emotion.service.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.emotion.dto.request.LoginRequest; +import com.emotion.dto.request.RegisterRequest; +import com.emotion.dto.response.AuthResponse; +import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.UserInfoResponse; +import com.emotion.service.AuthService; +import com.emotion.service.TokenService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import com.wf.captcha.SpecCaptcha; -import com.wf.captcha.base.Captcha; -import com.emotion.util.JwtUtil; import javax.servlet.http.HttpServletRequest; /** * 认证控制器 - * + * * @author emotion-museum - * @date 2025-07-22 + * @date 2025-07-23 */ @RestController @RequestMapping("/auth") public class AuthController { - private static final Logger log = LoggerFactory.getLogger(AuthController.class); - - // 验证码存储(生产环境应使用Redis) - private static final Map captchaStore = new ConcurrentHashMap<>(); + @Autowired + private AuthService authService; @Autowired - private UserService userService; - - @Autowired - private JwtUtil jwtUtil; + private TokenService tokenService; /** * 用户登录 */ @PostMapping("/login") - public Result> login(@RequestBody Map request) { - log.info("用户登录请求: {}", request.get("account")); - - try { - String account = request.get("account"); - String password = request.get("password"); - String captcha = request.get("captcha"); - String captchaKey = request.get("captchaKey"); - - if (account == null || password == null) { - return Result.error("账号和密码不能为空"); - } - - // 验证验证码 - if (captcha == null || captchaKey == null) { - return Result.error("验证码不能为空"); - } - - String storedCaptcha = captchaStore.get(captchaKey); - if (storedCaptcha == null) { - return Result.error("验证码已过期"); - } - - if (!storedCaptcha.equals(captcha.toLowerCase())) { - captchaStore.remove(captchaKey); // 验证失败后移除验证码 - return Result.error("验证码错误"); - } - - // 验证成功后移除验证码 - captchaStore.remove(captchaKey); - - // 查找用户 - User user = userService.findByAccount(account); - if (user == null) { - return Result.error("用户不存在"); - } - - // 验证密码 - if (!userService.validatePassword(password, user.getPassword())) { - return Result.error("密码错误"); - } - - // 更新最后活跃时间 - userService.updateLastActiveTime(user.getId()); - - // 生成JWT token - String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername()); - String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername()); - - // 构建响应 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("expiresIn", 86400L); - - Map userInfo = new HashMap<>(); - userInfo.put("id", user.getId()); - userInfo.put("username", user.getUsername()); - userInfo.put("account", user.getAccount()); - userInfo.put("nickname", user.getNickname()); - userInfo.put("avatar", user.getAvatar()); - userInfo.put("status", user.getStatus()); - - response.put("userInfo", userInfo); - response.put("loginTime", LocalDateTime.now()); - - return Result.success("登录成功", response); - } catch (Exception e) { - log.error("用户登录失败: {}", e.getMessage()); - return Result.error("登录失败: " + e.getMessage()); - } + public Result login(@RequestBody @Validated LoginRequest request) { + AuthResponse response = authService.login(request); + return Result.success("登录成功", response); } /** * 用户注册 */ @PostMapping("/register") - public Result> register(@RequestBody Map request) { - log.info("用户注册请求: {}", request.get("account")); - - try { - String account = request.get("account"); - String password = request.get("password"); - String username = request.get("username"); - String email = request.get("email"); - String phone = request.get("phone"); - String nickname = request.get("nickname"); - String captcha = request.get("captcha"); - String captchaKey = request.get("captchaKey"); - - if (account == null || password == null) { - return Result.error("账号和密码不能为空"); - } - - // 验证验证码 - if (captcha == null || captchaKey == null) { - return Result.error("验证码不能为空"); - } - - String storedCaptcha = captchaStore.get(captchaKey); - if (storedCaptcha == null) { - return Result.error("验证码已过期"); - } - - if (!storedCaptcha.equals(captcha.toLowerCase())) { - captchaStore.remove(captchaKey); // 验证失败后移除验证码 - return Result.error("验证码错误"); - } - - // 验证成功后移除验证码 - captchaStore.remove(captchaKey); - - // 检查账号是否已存在 - if (userService.accountExists(account)) { - return Result.error("账号已存在"); - } - - // 创建用户 - User user = new User(); - user.setAccount(account); - user.setPassword(password); - user.setUsername(username != null ? username : account); - user.setEmail(email); - user.setPhone(phone); - user.setNickname(nickname != null ? nickname : username != null ? username : account); - - User createdUser = userService.createUser(user); - - // 生成JWT token(注册成功后自动登录) - String accessToken = jwtUtil.generateToken(createdUser.getId(), createdUser.getUsername()); - String refreshToken = jwtUtil.generateRefreshToken(createdUser.getId(), createdUser.getUsername()); - - // 构建用户信息 - Map userInfo = new HashMap<>(); - userInfo.put("id", createdUser.getId()); - userInfo.put("username", createdUser.getUsername()); - userInfo.put("account", createdUser.getAccount()); - userInfo.put("nickname", createdUser.getNickname()); - userInfo.put("avatar", createdUser.getAvatar()); - userInfo.put("status", createdUser.getStatus()); - userInfo.put("createTime", createdUser.getCreateTime()); - - // 构建完整响应(包含token信息) - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("expiresIn", 86400L); // 24小时 - response.put("userInfo", userInfo); - response.put("loginTime", LocalDateTime.now()); - - log.info("用户注册并自动登录成功: {}", createdUser.getAccount()); - return Result.success("注册成功,已自动登录", response); - } catch (Exception e) { - log.error("用户注册失败: {}", e.getMessage()); - return Result.error("注册失败: " + e.getMessage()); - } + public Result register(@RequestBody @Validated RegisterRequest request) { + AuthResponse response = authService.register(request); + return Result.success("注册成功", response); } /** * 获取当前用户信息 */ - @GetMapping("/user-info") - public Result> getCurrentUserInfo(HttpServletRequest request) { - try { - // 从请求属性中获取用户信息(由JWT拦截器设置) - String userId = (String) request.getAttribute("userId"); - String username = (String) request.getAttribute("username"); - - if (userId == null) { - return Result.error("用户未登录"); - } - - // 根据用户ID获取完整用户信息 - User user = userService.findById(userId); - if (user == null) { - return Result.error("用户不存在"); - } - - // 构建用户信息响应 - Map userInfo = new HashMap<>(); - userInfo.put("id", user.getId()); - userInfo.put("username", user.getUsername()); - userInfo.put("account", user.getAccount()); - userInfo.put("nickname", user.getNickname()); - userInfo.put("avatar", user.getAvatar()); - userInfo.put("email", user.getEmail()); - userInfo.put("phone", user.getPhone()); - userInfo.put("status", user.getStatus()); - userInfo.put("createTime", user.getCreateTime()); - - return Result.success("获取用户信息成功", userInfo); - } catch (Exception e) { - log.error("获取用户信息失败: {}", e.getMessage()); - return Result.error("获取用户信息失败"); - } + @GetMapping("/user/info") + public Result getCurrentUserInfo(HttpServletRequest request) { + String token = extractToken(request); + UserInfoResponse userInfo = tokenService.getUserInfoByToken(token); + return Result.success(userInfo); } /** - * 获取验证码 + * 生成验证码 */ @GetMapping("/captcha") - public Result> getCaptcha() { - log.info("获取验证码请求"); - - try { - // 生成验证码 - SpecCaptcha captcha = new SpecCaptcha(130, 48, 4); - captcha.setCharType(Captcha.TYPE_DEFAULT); - - // 生成验证码key - String captchaKey = "captcha_" + System.currentTimeMillis(); - String captchaText = captcha.text().toLowerCase(); - - // 存储验证码(5分钟过期) - captchaStore.put(captchaKey, captchaText); - - // 5分钟后清理验证码 - new Thread(() -> { - try { - Thread.sleep(300000); // 5分钟 - captchaStore.remove(captchaKey); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }).start(); - - Map response = new HashMap<>(); - response.put("key", captchaKey); - response.put("image", captcha.toBase64().replace("data:image/png;base64,", "")); - response.put("expireTime", 300); - - log.info("生成验证码成功,key: {}, text: {}", captchaKey, captchaText); - - return Result.success("获取验证码成功", response); - } catch (Exception e) { - log.error("获取验证码失败: {}", e.getMessage()); - return Result.error("获取验证码失败"); - } + public Result generateCaptcha() { + CaptchaResponse response = authService.generateCaptcha(); + return Result.success(response); } /** * 用户登出 */ @PostMapping("/logout") - public Result logout(@RequestBody Map request) { - log.info("用户登出请求: {}", request.get("userId")); - return Result.success("登出成功"); + public Result logout(HttpServletRequest request) { + String token = extractToken(request); + authService.logoutByToken(token); + return Result.success(); } -} + + /** + * 刷新访问令牌 + */ + @PostMapping("/refresh") + public Result refreshToken(@RequestParam String refreshToken) { + AuthResponse response = authService.refreshToken(refreshToken); + return Result.success("令牌刷新成功", response); + } + + /** + * 验证访问令牌 + */ + @GetMapping("/validate") + public Result validateToken(HttpServletRequest request) { + String token = extractToken(request); + if (token == null) { + return Result.success(false); + } + + boolean isValid = authService.validateToken(token); + return Result.success(isValid); + } + + /** + * 获取用户名(通过令牌) + */ + @GetMapping("/username") + public Result getUsernameFromToken(HttpServletRequest request) { + String token = extractToken(request); + String username = tokenService.getUsernameByToken(token); + return Result.success(username); + } + + /** + * 从请求中提取访问令牌 + */ + private String extractToken(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + + // 也可以从请求参数中获取 + String tokenParam = request.getParameter("token"); + if (tokenParam != null && !tokenParam.trim().isEmpty()) { + return tokenParam.trim(); + } + + return null; + } +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/CommentController.java b/backend-single/src/main/java/com/emotion/controller/CommentController.java new file mode 100644 index 0000000..34c33ba --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/CommentController.java @@ -0,0 +1,249 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.Comment; +import com.emotion.service.CommentService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 评论控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/comment") +public class CommentController { + + @Autowired + private CommentService commentService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询评论 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = commentService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据帖子ID分页查询评论 + */ + @GetMapping("/post/{postId}/page") + public Result> getPageByPostId(@PathVariable String postId, @Validated PageRequest request) { + IPage page = commentService.getPageByPostId(request, postId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询评论 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = commentService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取评论 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + Comment comment = commentService.getById(id); + if (comment == null) { + return Result.notFound("评论不存在"); + } + return Result.success(convertToResponse(comment)); + } + + /** + * 创建评论 + */ + @PostMapping + public Result create(@RequestBody @Validated CommentCreateRequest request) { + Comment comment = commentService.createComment( + request.getPostId(), + request.getUserId(), + request.getContent(), + request.getReplyToId() + ); + return Result.success(convertToResponse(comment)); + } + + /** + * 更新评论 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody Comment comment) { + comment.setId(id); + boolean updated = commentService.updateById(comment); + if (!updated) { + return Result.error("更新失败"); + } + Comment updatedComment = commentService.getById(id); + return Result.success(convertToResponse(updatedComment)); + } + + /** + * 删除评论 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = commentService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据帖子ID查询顶级评论 + */ + @GetMapping("/post/{postId}/top-level") + public Result> getTopLevelCommentsByPostId(@PathVariable String postId) { + List comments = commentService.getTopLevelCommentsByPostId(postId); + List responses = comments.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据评论ID查询回复 + */ + @GetMapping("/{commentId}/replies") + public Result> getRepliesByCommentId(@PathVariable String commentId) { + List replies = commentService.getRepliesByCommentId(commentId); + List responses = replies.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 统计帖子的评论数量 + */ + @GetMapping("/post/{postId}/count") + public Result countByPostId(@PathVariable String postId) { + Long count = commentService.countByPostId(postId); + return Result.success(count); + } + + /** + * 点赞评论 + */ + @PutMapping("/{id}/like") + public Result likeComment(@PathVariable String id) { + boolean updated = commentService.updateLikes(id, 1); + if (!updated) { + return Result.error("点赞失败"); + } + return Result.success(); + } + + /** + * 取消点赞评论 + */ + @PutMapping("/{id}/unlike") + public Result unlikeComment(@PathVariable String id) { + boolean updated = commentService.updateLikes(id, -1); + if (!updated) { + return Result.error("取消点赞失败"); + } + return Result.success(); + } + + /** + * 转换为响应对象 + */ + 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; + } + + /** + * 评论创建请求 + */ + @lombok.Data + public static class CommentCreateRequest { + @NotBlank(message = "帖子ID不能为空") + private String postId; + + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "评论内容不能为空") + private String content; + + private String replyToId; + } + + /** + * 评论响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class CommentResponse extends BaseResponse { + private String postId; + private String userId; + private String content; + private String replyToId; + private Integer likes; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/CommunityPostController.java b/backend-single/src/main/java/com/emotion/controller/CommunityPostController.java new file mode 100644 index 0000000..00110b0 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/CommunityPostController.java @@ -0,0 +1,289 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.CommunityPost; +import com.emotion.service.CommunityPostService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 社区帖子控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/community-post") +public class CommunityPostController { + + @Autowired + private CommunityPostService communityPostService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询帖子 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = communityPostService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 分页查询公开帖子 + */ + @GetMapping("/public/page") + public Result> getPublicPostsPage(@Validated PageRequest request) { + IPage page = communityPostService.getPublicPostsPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询帖子 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = communityPostService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取帖子 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + CommunityPost post = communityPostService.getById(id); + if (post == null) { + return Result.notFound("帖子不存在"); + } + // 增加浏览数 + communityPostService.incrementViewCount(id); + return Result.success(convertToResponse(post)); + } + + /** + * 创建帖子 + */ + @PostMapping + public Result create(@RequestBody @Validated CommunityPostCreateRequest request) { + CommunityPost post = communityPostService.createPost( + request.getUserId(), + request.getTitle(), + request.getContent(), + request.getType(), + request.getLocationId(), + request.getTags(), + request.getIsPrivate() + ); + return Result.success(convertToResponse(post)); + } + + /** + * 更新帖子 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody CommunityPost post) { + post.setId(id); + boolean updated = communityPostService.updateById(post); + if (!updated) { + return Result.error("更新失败"); + } + CommunityPost updatedPost = communityPostService.getById(id); + return Result.success(convertToResponse(updatedPost)); + } + + /** + * 删除帖子 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = communityPostService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据类型查询帖子 + */ + @GetMapping("/type/{type}") + public Result> getByType(@PathVariable String type) { + List posts = communityPostService.getByType(type); + List responses = posts.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据地点ID查询帖子 + */ + @GetMapping("/location/{locationId}") + public Result> getByLocationId(@PathVariable String locationId) { + List posts = communityPostService.getByLocationId(locationId); + List responses = posts.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询最受欢迎的帖子 + */ + @GetMapping("/popular") + public Result> getMostLikedPosts(@RequestParam(defaultValue = "10") Integer limit) { + List posts = communityPostService.getMostLikedPosts(limit); + List responses = posts.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询最新的帖子 + */ + @GetMapping("/latest") + public Result> getLatestPosts(@RequestParam(defaultValue = "10") Integer limit) { + List posts = communityPostService.getLatestPosts(limit); + List responses = posts.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 点赞帖子 + */ + @PutMapping("/{id}/like") + public Result likePost(@PathVariable String id) { + boolean updated = communityPostService.updateLikes(id, 1); + if (!updated) { + return Result.error("点赞失败"); + } + return Result.success(); + } + + /** + * 取消点赞帖子 + */ + @PutMapping("/{id}/unlike") + public Result unlikePost(@PathVariable String id) { + boolean updated = communityPostService.updateLikes(id, -1); + if (!updated) { + return Result.error("取消点赞失败"); + } + return Result.success(); + } + + /** + * 根据标签搜索帖子 + */ + @GetMapping("/search/tag") + public Result> getByTag(@RequestParam String tag) { + List posts = communityPostService.getByTag(tag); + List responses = posts.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 转换为响应对象 + */ + 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; + } + + /** + * 帖子创建请求 + */ + @lombok.Data + public static class CommunityPostCreateRequest { + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "标题不能为空") + private String title; + + @NotBlank(message = "内容不能为空") + private String content; + + private String type; + private String locationId; + private String tags; + private Integer isPrivate; + } + + /** + * 帖子响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class CommunityPostResponse extends BaseResponse { + private String userId; + private String title; + private String content; + private String type; + private String locationId; + private String tags; + private Integer likes; + private Integer viewCount; + private Integer commentCount; + private Integer isPrivate; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/ConversationController.java b/backend-single/src/main/java/com/emotion/controller/ConversationController.java new file mode 100644 index 0000000..40a98b1 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/ConversationController.java @@ -0,0 +1,283 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.Conversation; +import com.emotion.service.ConversationService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.constraints.NotBlank; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 对话控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/conversation") +public class ConversationController { + + @Autowired + private ConversationService conversationService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询对话 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = conversationService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询对话 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = conversationService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取对话 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + Conversation conversation = conversationService.getById(id); + if (conversation == null) { + return Result.notFound("对话不存在"); + } + return Result.success(convertToResponse(conversation)); + } + + /** + * 创建对话 + */ + @PostMapping + public Result create(@RequestBody @Validated ConversationCreateRequest request, HttpServletRequest httpRequest) { + String clientIp = getClientIp(httpRequest); + Conversation conversation = conversationService.createConversation( + request.getUserId(), + request.getTitle(), + request.getType(), + clientIp + ); + return Result.success(convertToResponse(conversation)); + } + + /** + * 更新对话 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody Conversation conversation) { + conversation.setId(id); + boolean updated = conversationService.updateById(conversation); + if (!updated) { + return Result.error("更新失败"); + } + Conversation updatedConversation = conversationService.getById(id); + return Result.success(convertToResponse(updatedConversation)); + } + + /** + * 删除对话 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = conversationService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据用户ID查询对话 + */ + @GetMapping("/user/{userId}") + public Result> getByUserId(@PathVariable String userId) { + List conversations = conversationService.getByUserId(userId); + List responses = conversations.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据类型查询对话 + */ + @GetMapping("/type/{type}") + public Result> getByType(@PathVariable String type) { + List conversations = conversationService.getByType(type); + List responses = conversations.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据状态查询对话 + */ + @GetMapping("/status/{status}") + public Result> getByStatus(@PathVariable String status) { + List conversations = conversationService.getByStatus(status); + List responses = conversations.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询活跃对话 + */ + @GetMapping("/active") + public Result> getActiveConversations() { + List conversations = conversationService.getActiveConversations(); + List responses = conversations.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询已归档对话 + */ + @GetMapping("/archived") + public Result> getArchivedConversations() { + List conversations = conversationService.getArchivedConversations(); + List responses = conversations.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 归档对话 + */ + @PutMapping("/{id}/archive") + public Result archiveConversation(@PathVariable String id) { + boolean archived = conversationService.archiveConversation(id); + if (!archived) { + return Result.error("归档失败"); + } + return Result.success(); + } + + /** + * 激活对话 + */ + @PutMapping("/{id}/activate") + public Result activateConversation(@PathVariable String id) { + boolean activated = conversationService.activateConversation(id); + if (!activated) { + return Result.error("激活失败"); + } + return Result.success(); + } + + /** + * 统计用户对话数量 + */ + @GetMapping("/user/{userId}/count") + public Result countByUserId(@PathVariable String userId) { + Long count = conversationService.countByUserId(userId); + return Result.success(count); + } + + /** + * 获取客户端IP + */ + private String getClientIp(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0]; + } + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + return request.getRemoteAddr(); + } + + /** + * 转换为响应对象 + */ + private ConversationResponse convertToResponse(Conversation conversation) { + ConversationResponse response = new ConversationResponse(); + BeanUtils.copyProperties(conversation, response); + response.setId(conversation.getId()); + 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.getLastMessageTime() != null) { + response.setLastMessageTime(conversation.getLastMessageTime().format(DATE_TIME_FORMATTER)); + } + return response; + } + + /** + * 对话创建请求 + */ + @lombok.Data + public static class ConversationCreateRequest { + @NotBlank(message = "用户ID不能为空") + private String userId; + + private String title; + private String type; + } + + /** + * 对话响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class ConversationResponse extends BaseResponse { + private String userId; + private String cozeConversationId; + private String title; + private String type; + private String status; + private Integer messageCount; + private String lastMessageTime; + private String clientIp; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java b/backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java new file mode 100644 index 0000000..bcd5e45 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java @@ -0,0 +1,307 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.CozeApiCall; +import com.emotion.service.CozeApiCallService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Coze API调用记录控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/coze-api-call") +public class CozeApiCallController { + + @Autowired + private CozeApiCallService cozeApiCallService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询API调用记录 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = cozeApiCallService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据会话ID分页查询API调用记录 + */ + @GetMapping("/conversation/{conversationId}/page") + public Result> getPageByConversationId(@PathVariable String conversationId, @Validated PageRequest request) { + IPage page = cozeApiCallService.getPageByConversationId(request, conversationId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询API调用记录 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = cozeApiCallService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取API调用记录 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + CozeApiCall apiCall = cozeApiCallService.getById(id); + if (apiCall == null) { + return Result.notFound("API调用记录不存在"); + } + return Result.success(convertToResponse(apiCall)); + } + + /** + * 根据Bot ID查询API调用记录 + */ + @GetMapping("/bot/{botId}") + public Result> getByBotId(@PathVariable String botId) { + List apiCalls = cozeApiCallService.getByBotId(botId); + List responses = apiCalls.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据状态查询API调用记录 + */ + @GetMapping("/status/{status}") + public Result> getByStatus(@PathVariable String status) { + List apiCalls = cozeApiCallService.getByStatus(status); + List responses = apiCalls.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据请求类型查询API调用记录 + */ + @GetMapping("/request-type/{requestType}") + public Result> getByRequestType(@PathVariable String requestType) { + List apiCalls = cozeApiCallService.getByRequestType(requestType); + List responses = apiCalls.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 统计用户的API调用次数 + */ + @GetMapping("/user/{userId}/count") + public Result countByUserId(@PathVariable String userId) { + Long count = cozeApiCallService.countByUserId(userId); + return Result.success(count); + } + + /** + * 统计Bot的API调用次数 + */ + @GetMapping("/bot/{botId}/count") + public Result countByBotId(@PathVariable String botId) { + Long count = cozeApiCallService.countByBotId(botId); + return Result.success(count); + } + + /** + * 统计指定状态的API调用次数 + */ + @GetMapping("/status/{status}/count") + public Result countByStatus(@PathVariable String status) { + Long count = cozeApiCallService.countByStatus(status); + return Result.success(count); + } + + /** + * 统计用户的Token使用量 + */ + @GetMapping("/user/{userId}/tokens") + public Result sumTokensByUserId(@PathVariable String userId) { + Long totalTokens = cozeApiCallService.sumTokensByUserId(userId); + return Result.success(totalTokens); + } + + /** + * 统计用户的API调用费用 + */ + @GetMapping("/user/{userId}/cost") + public Result sumCostByUserId(@PathVariable String userId) { + java.math.BigDecimal totalCost = cozeApiCallService.sumCostByUserId(userId); + return Result.success(totalCost); + } + + /** + * 查询失败的API调用记录 + */ + @GetMapping("/failed") + public Result> getFailedCalls() { + List apiCalls = cozeApiCallService.getFailedCalls(); + List responses = apiCalls.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询超时的API调用记录 + */ + @GetMapping("/timeout") + public Result> getTimeoutCalls() { + List apiCalls = cozeApiCallService.getTimeoutCalls(); + List responses = apiCalls.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据追踪ID查询API调用记录 + */ + @GetMapping("/trace/{traceId}") + public Result getByTraceId(@PathVariable String traceId) { + CozeApiCall apiCall = cozeApiCallService.getByTraceId(traceId); + if (apiCall == null) { + return Result.notFound("API调用记录不存在"); + } + return Result.success(convertToResponse(apiCall)); + } + + /** + * 创建API调用记录 + */ + @PostMapping + public Result create(@RequestBody CozeApiCall apiCall) { + boolean saved = cozeApiCallService.save(apiCall); + if (!saved) { + return Result.error("创建失败"); + } + return Result.success(convertToResponse(apiCall)); + } + + /** + * 更新API调用记录 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody CozeApiCall apiCall) { + apiCall.setId(id); + boolean updated = cozeApiCallService.updateById(apiCall); + if (!updated) { + return Result.error("更新失败"); + } + CozeApiCall updatedApiCall = cozeApiCallService.getById(id); + return Result.success(convertToResponse(updatedApiCall)); + } + + /** + * 删除API调用记录 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = cozeApiCallService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 转换为响应对象 + */ + 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)); + } + return response; + } + + /** + * API调用记录响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class CozeApiCallResponse extends BaseResponse { + private String conversationId; + private String messageId; + private String userId; + private String botId; + private String requestType; + private String requestUrl; + private String requestBody; + private Integer responseStatus; + private String responseBody; + private String aiReply; + private Integer totalTokens; + private java.math.BigDecimal cost; + private String status; + private String finalStatus; + private String startTime; + private String endTime; + private String traceId; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java b/backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java new file mode 100644 index 0000000..a0be461 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java @@ -0,0 +1,287 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.EmotionAnalysis; +import com.emotion.service.EmotionAnalysisService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 情绪分析控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/emotion-analysis") +public class EmotionAnalysisController { + + @Autowired + private EmotionAnalysisService emotionAnalysisService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询情绪分析记录 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = emotionAnalysisService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询情绪分析记录 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = emotionAnalysisService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取情绪分析记录 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + EmotionAnalysis analysis = emotionAnalysisService.getById(id); + if (analysis == null) { + return Result.notFound("情绪分析记录不存在"); + } + return Result.success(convertToResponse(analysis)); + } + + /** + * 根据消息ID获取情绪分析记录 + */ + @GetMapping("/message/{messageId}") + public Result getByMessageId(@PathVariable String messageId) { + EmotionAnalysis analysis = emotionAnalysisService.getByMessageId(messageId); + if (analysis == null) { + return Result.notFound("情绪分析记录不存在"); + } + return Result.success(convertToResponse(analysis)); + } + + /** + * 创建情绪分析记录 + */ + @PostMapping + public Result create(@RequestBody @Validated EmotionAnalysisCreateRequest request) { + EmotionAnalysis analysis = emotionAnalysisService.createEmotionAnalysis( + request.getMessageId(), + request.getUserId(), + request.getPrimaryEmotion(), + request.getPolarity(), + request.getIntensity(), + request.getConfidence() + ); + return Result.success(convertToResponse(analysis)); + } + + /** + * 更新情绪分析记录 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody EmotionAnalysis analysis) { + analysis.setId(id); + boolean updated = emotionAnalysisService.updateById(analysis); + if (!updated) { + return Result.error("更新失败"); + } + EmotionAnalysis updatedAnalysis = emotionAnalysisService.getById(id); + return Result.success(convertToResponse(updatedAnalysis)); + } + + /** + * 删除情绪分析记录 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = emotionAnalysisService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据主要情绪查询分析记录 + */ + @GetMapping("/emotion/{primaryEmotion}") + public Result> getByPrimaryEmotion(@PathVariable String primaryEmotion) { + List analyses = emotionAnalysisService.getByPrimaryEmotion(primaryEmotion); + List responses = analyses.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据情绪极性查询分析记录 + */ + @GetMapping("/polarity/{polarity}") + public Result> getByPolarity(@PathVariable String polarity) { + List analyses = emotionAnalysisService.getByPolarity(polarity); + List responses = analyses.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据用户ID和情绪类型查询分析记录 + */ + @GetMapping("/user/{userId}/emotion/{primaryEmotion}") + public Result> getByUserIdAndEmotion(@PathVariable String userId, @PathVariable String primaryEmotion) { + List analyses = emotionAnalysisService.getByUserIdAndEmotion(userId, primaryEmotion); + List responses = analyses.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据时间范围查询用户情绪分析记录 + */ + @GetMapping("/user/{userId}/time-range") + public Result> getByUserIdAndTimeRange( + @PathVariable String userId, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) { + List analyses = emotionAnalysisService.getByUserIdAndTimeRange(userId, startTime, endTime); + List responses = analyses.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 统计用户的情绪分析记录数量 + */ + @GetMapping("/user/{userId}/count") + public Result countByUserId(@PathVariable String userId) { + Long count = emotionAnalysisService.countByUserId(userId); + return Result.success(count); + } + + /** + * 查询用户最近的情绪分析记录 + */ + @GetMapping("/user/{userId}/recent") + public Result> getRecentByUserId(@PathVariable String userId, @RequestParam(defaultValue = "10") Integer limit) { + List analyses = emotionAnalysisService.getRecentByUserId(userId, limit); + List responses = analyses.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询用户的平均情绪强度 + */ + @GetMapping("/user/{userId}/avg-intensity") + public Result getAvgIntensityByUserId(@PathVariable String userId) { + Double avgIntensity = emotionAnalysisService.getAvgIntensityByUserId(userId); + return Result.success(avgIntensity); + } + + /** + * 查询用户最常见的情绪类型 + */ + @GetMapping("/user/{userId}/most-frequent-emotion") + public Result getMostFrequentEmotionByUserId(@PathVariable String userId) { + String emotion = emotionAnalysisService.getMostFrequentEmotionByUserId(userId); + return Result.success(emotion); + } + + /** + * 转换为响应对象 + */ + 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)); + } + return response; + } + + /** + * 情绪分析创建请求 + */ + @lombok.Data + public static class EmotionAnalysisCreateRequest { + @NotBlank(message = "消息ID不能为空") + private String messageId; + + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "主要情绪不能为空") + private String primaryEmotion; + + private String polarity; + + @NotNull(message = "情绪强度不能为空") + private Double intensity; + + @NotNull(message = "置信度不能为空") + private Double confidence; + } + + /** + * 情绪分析响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class EmotionAnalysisResponse extends BaseResponse { + private String messageId; + private String userId; + private String primaryEmotion; + private String polarity; + private Double intensity; + private Double confidence; + private String emotionDetails; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/MessageController.java b/backend-single/src/main/java/com/emotion/controller/MessageController.java index 3222d96..512339b 100644 --- a/backend-single/src/main/java/com/emotion/controller/MessageController.java +++ b/backend-single/src/main/java/com/emotion/controller/MessageController.java @@ -1,220 +1,173 @@ package com.emotion.controller; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.emotion.common.PageResult; import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; import com.emotion.entity.Message; -import com.emotion.service.IMessageService; -import lombok.extern.slf4j.Slf4j; +import com.emotion.service.MessageService; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.util.HashMap; +import javax.validation.constraints.NotBlank; +import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; /** * 消息控制器 - * + * * @author emotion-museum * @date 2025-07-23 */ -@Slf4j @RestController -@RequestMapping("/api/message") +@RequestMapping("/message") public class MessageController { - + @Autowired - private IMessageService messageService; - + private MessageService messageService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /** - * 保存消息 + * 分页查询消息 */ - @PostMapping("/save") - public Result saveMessage(@RequestBody Map request) { - try { - String conversationId = request.get("conversationId"); - String content = request.get("content"); - String type = request.get("type"); - String sender = request.get("sender"); - - if (content == null || content.trim().isEmpty()) { - return Result.error("消息内容不能为空"); - } - - if (sender == null || sender.trim().isEmpty()) { - return Result.error("发送者不能为空"); - } - - Message message = messageService.saveMessage(conversationId, content, type, sender); - if (message != null) { - return Result.success(message); - } else { - return Result.error("保存消息失败"); - } - - } catch (Exception e) { - log.error("保存消息失败", e); - return Result.error("保存消息失败:" + e.getMessage()); - } + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = messageService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); } - + /** * 根据会话ID分页查询消息 */ + @GetMapping("/conversation/{conversationId}/page") + public Result> getPageByConversationId(@PathVariable String conversationId, @Validated PageRequest request) { + IPage page = messageService.getPageByConversationId(request, conversationId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取消息 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + Message message = messageService.getById(id); + if (message == null) { + return Result.notFound("消息不存在"); + } + return Result.success(convertToResponse(message)); + } + + /** + * 创建消息 + */ + @PostMapping + public Result create(@RequestBody @Validated MessageCreateRequest request) { + Message message = messageService.createMessage( + request.getConversationId(), + request.getUserId(), + request.getContent(), + request.getContentType(), + request.getSenderType(), + request.getSenderId() + ); + return Result.success(convertToResponse(message)); + } + + /** + * 根据会话ID查询消息 + */ @GetMapping("/conversation/{conversationId}") - public Result> getMessagesByConversationId( - @PathVariable String conversationId, - @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "20") Integer size) { - try { - Page page = new Page<>(current, size); - IPage result = messageService.getByConversationId(page, conversationId); - return Result.success(result); - } catch (Exception e) { - log.error("查询会话消息失败", e); - return Result.error("查询会话消息失败:" + e.getMessage()); - } + public Result> getByConversationId(@PathVariable String conversationId) { + List messages = messageService.getByConversationId(conversationId); + List responses = messages.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); } - - /** - * 根据发送者分页查询消息 - */ - @GetMapping("/sender/{sender}") - public Result> getMessagesBySender( - @PathVariable String sender, - @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "20") Integer size) { - try { - Page page = new Page<>(current, size); - IPage result = messageService.getBySender(page, sender); - return Result.success(result); - } catch (Exception e) { - log.error("查询发送者消息失败", e); - return Result.error("查询发送者消息失败:" + e.getMessage()); - } - } - - /** - * 查询会话的最后一条消息 - */ - @GetMapping("/last/{conversationId}") - public Result getLastMessage(@PathVariable String conversationId) { - try { - Message message = messageService.getLastMessageByConversationId(conversationId); - return Result.success(message); - } catch (Exception e) { - log.error("查询最后一条消息失败", e); - return Result.error("查询最后一条消息失败:" + e.getMessage()); - } - } - + /** * 统计会话消息数量 */ - @GetMapping("/count/{conversationId}") - public Result countMessages(@PathVariable String conversationId) { - try { - Long count = messageService.countByConversationId(conversationId); - return Result.success(count); - } catch (Exception e) { - log.error("统计消息数量失败", e); - return Result.error("统计消息数量失败:" + e.getMessage()); - } + @GetMapping("/conversation/{conversationId}/count") + public Result countByConversationId(@PathVariable String conversationId) { + Long count = messageService.countByConversationId(conversationId); + return Result.success(count); } - + /** - * 更新消息状态 + * 转换为响应对象 */ - @PutMapping("/{messageId}/status") - public Result updateStatus(@PathVariable String messageId, - @RequestBody Map request) { - try { - String status = request.get("status"); - if (status == null || status.trim().isEmpty()) { - return Result.error("状态不能为空"); - } - - boolean success = messageService.updateStatus(messageId, status); - return Result.success(success); - } catch (Exception e) { - log.error("更新消息状态失败", e); - return Result.error("更新消息状态失败:" + e.getMessage()); + 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; } - + /** - * 标记消息为已读 + * 消息创建请求 */ - @PutMapping("/{messageId}/read") - public Result markAsRead(@PathVariable String messageId) { - try { - boolean success = messageService.updateReadStatus(messageId, 1); - return Result.success(success); - } catch (Exception e) { - log.error("标记消息已读失败", e); - return Result.error("标记消息已读失败:" + e.getMessage()); - } + @lombok.Data + public static class MessageCreateRequest { + @NotBlank(message = "会话ID不能为空") + private String conversationId; + + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "消息内容不能为空") + private String content; + + private String contentType; + private String senderType; + private String senderId; } - + /** - * 批量标记会话消息为已读 + * 消息响应类 */ - @PutMapping("/conversation/{conversationId}/read") - public Result markConversationAsRead(@PathVariable String conversationId) { - try { - boolean success = messageService.markConversationMessagesAsRead(conversationId); - return Result.success(success); - } catch (Exception e) { - log.error("批量标记消息已读失败", e); - return Result.error("批量标记消息已读失败:" + e.getMessage()); - } + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class MessageResponse extends BaseResponse { + private String conversationId; + private String content; + private String type; + private String sender; + private Integer isRead; + private String aiReply; + private String emotionAnalysis; } - - /** - * 获取消息统计信息 - */ - @GetMapping("/stats") - public Result> getMessageStats( - @RequestParam(required = false) String conversationId, - @RequestParam(required = false) String sender) { - try { - Map stats = new HashMap<>(); - - if (conversationId != null && !conversationId.trim().isEmpty()) { - Long conversationCount = messageService.countByConversationId(conversationId); - stats.put("conversationMessageCount", conversationCount); - - Message lastMessage = messageService.getLastMessageByConversationId(conversationId); - stats.put("lastMessage", lastMessage); - } - - if (sender != null && !sender.trim().isEmpty()) { - Long senderCount = messageService.countBySender(sender); - stats.put("senderMessageCount", senderCount); - } - - stats.put("timestamp", System.currentTimeMillis()); - - return Result.success(stats); - } catch (Exception e) { - log.error("获取消息统计失败", e); - return Result.error("获取消息统计失败:" + e.getMessage()); - } - } - - /** - * 删除会话的所有消息 - */ - @DeleteMapping("/conversation/{conversationId}") - public Result deleteConversationMessages(@PathVariable String conversationId) { - try { - boolean success = messageService.deleteByConversationId(conversationId); - return Result.success(success); - } catch (Exception e) { - log.error("删除会话消息失败", e); - return Result.error("删除会话消息失败:" + e.getMessage()); - } - } -} +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/RewardController.java b/backend-single/src/main/java/com/emotion/controller/RewardController.java new file mode 100644 index 0000000..97d002d --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/RewardController.java @@ -0,0 +1,332 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.Reward; +import com.emotion.service.RewardService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 奖励控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/reward") +public class RewardController { + + @Autowired + private RewardService rewardService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询奖励 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = rewardService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID分页查询奖励 + */ + @GetMapping("/user/{userId}/page") + public Result> getPageByUserId(@PathVariable String userId, @Validated PageRequest request) { + IPage page = rewardService.getPageByUserId(request, userId); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据ID获取奖励 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + Reward reward = rewardService.getById(id); + if (reward == null) { + return Result.notFound("奖励不存在"); + } + return Result.success(convertToResponse(reward)); + } + + /** + * 创建奖励 + */ + @PostMapping + public Result create(@RequestBody @Validated RewardCreateRequest request) { + Reward reward = rewardService.createReward( + request.getUserId(), + request.getRewardType(), + request.getPoints(), + request.getSource(), + request.getDescription(), + request.getExpiredTime() + ); + return Result.success(convertToResponse(reward)); + } + + /** + * 更新奖励 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody Reward reward) { + reward.setId(id); + boolean updated = rewardService.updateById(reward); + if (!updated) { + return Result.error("更新失败"); + } + Reward updatedReward = rewardService.getById(id); + return Result.success(convertToResponse(updatedReward)); + } + + /** + * 删除奖励 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = rewardService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 根据用户ID查询奖励 + */ + @GetMapping("/user/{userId}") + public Result> getByUserId(@PathVariable String userId) { + List rewards = rewardService.getByUserId(userId); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据奖励类型查询奖励 + */ + @GetMapping("/type/{rewardType}") + public Result> getByRewardType(@PathVariable String rewardType) { + List rewards = rewardService.getByRewardType(rewardType); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 根据状态查询奖励 + */ + @GetMapping("/status/{status}") + public Result> getByStatus(@PathVariable String status) { + List rewards = rewardService.getByStatus(status); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询用户待领取的奖励 + */ + @GetMapping("/user/{userId}/pending") + public Result> getPendingRewardsByUserId(@PathVariable String userId) { + List rewards = rewardService.getPendingRewardsByUserId(userId); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询用户已领取的奖励 + */ + @GetMapping("/user/{userId}/claimed") + public Result> getClaimedRewardsByUserId(@PathVariable String userId) { + List rewards = rewardService.getClaimedRewardsByUserId(userId); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 统计用户的奖励数量 + */ + @GetMapping("/user/{userId}/count") + public Result countByUserId(@PathVariable String userId) { + Long count = rewardService.countByUserId(userId); + return Result.success(count); + } + + /** + * 统计用户的总积分 + */ + @GetMapping("/user/{userId}/total-points") + public Result sumPointsByUserId(@PathVariable String userId) { + Integer totalPoints = rewardService.sumPointsByUserId(userId); + return Result.success(totalPoints); + } + + /** + * 查询用户最近获得的奖励 + */ + @GetMapping("/user/{userId}/recent") + public Result> getRecentByUserId(@PathVariable String userId, @RequestParam(defaultValue = "10") Integer limit) { + List rewards = rewardService.getRecentByUserId(userId, limit); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 领取奖励 + */ + @PutMapping("/{id}/claim") + public Result claimReward(@PathVariable String id) { + boolean claimed = rewardService.updateStatus(id, "claimed", LocalDateTime.now()); + if (!claimed) { + return Result.error("领取失败"); + } + return Result.success(); + } + + /** + * 查询高积分奖励 + */ + @GetMapping("/high-points") + public Result> getHighPointsRewards(@RequestParam(defaultValue = "100") Integer minPoints) { + List rewards = rewardService.getHighPointsRewards(minPoints); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询已过期的奖励 + */ + @GetMapping("/expired") + public Result> getExpiredRewards() { + List rewards = rewardService.getExpiredRewards(); + List responses = rewards.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 批量更新过期奖励状态 + */ + @PutMapping("/update-expired") + public Result updateExpiredRewards() { + boolean updated = rewardService.updateExpiredRewards(); + if (!updated) { + return Result.error("更新失败"); + } + return Result.success(); + } + + /** + * 转换为响应对象 + */ + private RewardResponse convertToResponse(Reward reward) { + RewardResponse response = new RewardResponse(); + BeanUtils.copyProperties(reward, response); + response.setId(reward.getId()); + if (reward.getCreateTime() != null) { + response.setCreateTime(reward.getCreateTime().format(DATE_TIME_FORMATTER)); + } + if (reward.getUpdateTime() != null) { + response.setUpdateTime(reward.getUpdateTime().format(DATE_TIME_FORMATTER)); + } + if (reward.getEarnedTime() != null) { + response.setEarnedTime(reward.getEarnedTime().format(DATE_TIME_FORMATTER)); + } + if (reward.getClaimedTime() != null) { + response.setClaimedTime(reward.getClaimedTime().format(DATE_TIME_FORMATTER)); + } + if (reward.getExpiredTime() != null) { + response.setExpiredTime(reward.getExpiredTime().format(DATE_TIME_FORMATTER)); + } + return response; + } + + /** + * 奖励创建请求 + */ + @lombok.Data + public static class RewardCreateRequest { + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "奖励类型不能为空") + private String rewardType; + + @NotNull(message = "积分不能为空") + private Integer points; + + private String source; + private String description; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime expiredTime; + } + + /** + * 奖励响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class RewardResponse extends BaseResponse { + private String userId; + private String rewardType; + private Integer points; + private String source; + private String description; + private String status; + private String earnedTime; + private String claimedTime; + private String expiredTime; + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/UserController.java b/backend-single/src/main/java/com/emotion/controller/UserController.java index c0dea21..a33e55c 100644 --- a/backend-single/src/main/java/com/emotion/controller/UserController.java +++ b/backend-single/src/main/java/com/emotion/controller/UserController.java @@ -1,18 +1,25 @@ package com.emotion.controller; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.BasePageRequest; +import com.emotion.common.PageResult; import com.emotion.common.Result; +import com.emotion.dto.request.UserCreateRequest; +import com.emotion.dto.response.UserResponse; import com.emotion.entity.User; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.emotion.service.UserService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; /** * 用户控制器 - * + * * @author emotion-museum * @date 2025-07-22 */ @@ -20,100 +27,121 @@ import java.util.Map; @RequestMapping("/user") public class UserController { - private static final Logger log = LoggerFactory.getLogger(UserController.class); + @Autowired + private UserService userService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** - * 获取用户信息 + * 分页查询用户 */ - @GetMapping("/info/{userId}") - public Result> getUserInfo(@PathVariable String userId) { - log.info("获取用户信息: {}", userId); - - try { - // 模拟用户信息 - Map userInfo = new HashMap<>(); - userInfo.put("id", userId); - userInfo.put("username", "user" + userId); - userInfo.put("account", "user" + userId); - userInfo.put("nickname", "用户" + userId); - userInfo.put("avatar", "https://example.com/avatar/" + userId + ".jpg"); - userInfo.put("status", 1); - userInfo.put("memberLevel", "free"); - userInfo.put("totalDays", 30); - userInfo.put("createTime", LocalDateTime.now().minusDays(30)); - userInfo.put("lastActiveTime", LocalDateTime.now()); - - return Result.success(userInfo); - } catch (Exception e) { - log.error("获取用户信息失败: {}", e.getMessage()); - return Result.error("获取用户信息失败"); - } + @GetMapping("/page") + public Result> getPage(@Validated BasePageRequest request) { + IPage page = userService.getPage(request); + List userResponses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(userResponses); + + return Result.success(pageResult); } /** - * 更新用户信息 + * 根据ID获取用户信息 */ - @PutMapping("/info/{userId}") - public Result> updateUserInfo(@PathVariable String userId, - @RequestBody Map request) { - log.info("更新用户信息: {}", userId); - - try { - // 模拟更新用户信息 - Map userInfo = new HashMap<>(); - userInfo.put("id", userId); - userInfo.put("nickname", request.get("nickname")); - userInfo.put("avatar", request.get("avatar")); - userInfo.put("bio", request.get("bio")); - userInfo.put("gender", request.get("gender")); - userInfo.put("updateTime", LocalDateTime.now()); - - return Result.success("更新成功", userInfo); - } catch (Exception e) { - log.error("更新用户信息失败: {}", e.getMessage()); - return Result.error("更新用户信息失败"); + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + User user = userService.getById(id); + if (user == null) { + return Result.notFound("用户不存在"); } + return Result.success(convertToResponse(user)); } /** - * 更新最后活跃时间 + * 创建用户 */ - @PostMapping("/active/{userId}") - public Result updateLastActiveTime(@PathVariable String userId) { - log.info("更新最后活跃时间: {}", userId); - - try { - // 模拟更新活跃时间 - return Result.success("更新成功"); - } catch (Exception e) { - log.error("更新最后活跃时间失败: {}", e.getMessage()); + @PostMapping + public Result create(@Validated @RequestBody UserCreateRequest request) { + User user = userService.createUser( + request.getAccount(), + request.getUsername(), + request.getPassword(), + request.getEmail(), + request.getPhone() + ); + return Result.success(convertToResponse(user)); + } + + /** + * 更新用户 + */ + @PutMapping("/{id}") + public Result update(@PathVariable String id, @RequestBody User user) { + user.setId(id); + boolean updated = userService.updateById(user); + if (!updated) { return Result.error("更新失败"); } + User updatedUser = userService.getById(id); + return Result.success(convertToResponse(updatedUser)); } /** - * 获取用户统计信息 + * 删除用户 */ - @GetMapping("/stats/{userId}") - public Result> getUserStats(@PathVariable String userId) { - log.info("获取用户统计信息: {}", userId); - - try { - Map stats = new HashMap<>(); - stats.put("totalDays", 30); - stats.put("totalRecords", 45); - stats.put("totalConversations", 12); - stats.put("totalMessages", 156); - stats.put("selfAwareness", 75.5); - stats.put("emotionalResilience", 68.2); - stats.put("actionPower", 82.1); - stats.put("empathy", 79.3); - stats.put("lifeEnthusiasm", 85.7); - - return Result.success(stats); - } catch (Exception e) { - log.error("获取用户统计信息失败: {}", e.getMessage()); - return Result.error("获取统计信息失败"); + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + boolean deleted = userService.removeById(id); + if (!deleted) { + return Result.error("删除失败"); } + return Result.success(); } -} + + /** + * 根据账号查询用户 + */ + @GetMapping("/account/{account}") + public Result getByAccount(@PathVariable String account) { + User user = userService.getByAccount(account); + if (user == null) { + return Result.notFound("用户不存在"); + } + return Result.success(convertToResponse(user)); + } + + /** + * 统计用户数量 + */ + @GetMapping("/count/status/{status}") + public Result countByStatus(@PathVariable Integer status) { + Long count = userService.countByStatus(status); + return Result.success(count); + } + + /** + * 转换为响应对象 + */ + 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; + } +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/UserStatsController.java b/backend-single/src/main/java/com/emotion/controller/UserStatsController.java new file mode 100644 index 0000000..b095176 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/UserStatsController.java @@ -0,0 +1,270 @@ +package com.emotion.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.emotion.common.PageResult; +import com.emotion.common.Result; +import com.emotion.dto.request.PageRequest; +import com.emotion.dto.response.BaseResponse; +import com.emotion.entity.UserStats; +import com.emotion.service.UserStatsService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户统计控制器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestController +@RequestMapping("/user-stats") +public class UserStatsController { + + @Autowired + private UserStatsService userStatsService; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 分页查询用户统计 + */ + @GetMapping("/page") + public Result> getPage(@Validated PageRequest request) { + IPage page = userStatsService.getPage(request); + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setPages(page.getPages()); + pageResult.setRecords(responses); + + return Result.success(pageResult); + } + + /** + * 根据用户ID获取统计信息 + */ + @GetMapping("/user/{userId}") + public Result getByUserId(@PathVariable String userId) { + UserStats stats = userStatsService.getByUserId(userId); + if (stats == null) { + return Result.notFound("用户统计不存在"); + } + return Result.success(convertToResponse(stats)); + } + + /** + * 根据用户ID和统计类型获取统计信息 + */ + @GetMapping("/user/{userId}/type/{statsType}") + public Result getByUserIdAndStatsType(@PathVariable String userId, @PathVariable String statsType) { + UserStats stats = userStatsService.getByUserIdAndStatsType(userId, statsType); + if (stats == null) { + return Result.notFound("用户统计不存在"); + } + return Result.success(convertToResponse(stats)); + } + + /** + * 根据统计类型查询统计信息 + */ + @GetMapping("/type/{statsType}") + public Result> getByStatsType(@PathVariable String statsType) { + List statsList = userStatsService.getByStatsType(statsType); + List responses = statsList.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询用户的所有统计类型 + */ + @GetMapping("/user/{userId}/all") + public Result> getAllStatsByUserId(@PathVariable String userId) { + List statsList = userStatsService.getAllStatsByUserId(userId); + List responses = statsList.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询排名前N的用户统计 + */ + @GetMapping("/type/{statsType}/top") + public Result> getTopUsersByStatsType(@PathVariable String statsType, @RequestParam(defaultValue = "10") Integer limit) { + List statsList = userStatsService.getTopUsersByStatsType(statsType, limit); + List responses = statsList.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + return Result.success(responses); + } + + /** + * 查询用户在指定统计类型中的排名 + */ + @GetMapping("/user/{userId}/type/{statsType}/rank") + public Result getUserRankByStatsType(@PathVariable String userId, @PathVariable String statsType) { + Long rank = userStatsService.getUserRankByStatsType(userId, statsType); + return Result.success(rank); + } + + /** + * 创建或更新用户统计 + */ + @PostMapping + public Result createOrUpdate(@RequestBody @Validated UserStatsCreateRequest request) { + UserStats stats = userStatsService.createOrUpdateUserStats( + request.getUserId(), + request.getStatsType(), + request.getValue(), + request.getPeriod() + ); + return Result.success(convertToResponse(stats)); + } + + /** + * 更新用户统计值 + */ + @PutMapping("/user/{userId}/type/{statsType}") + public Result updateStatsValue(@PathVariable String userId, @PathVariable String statsType, @RequestParam Double value) { + boolean updated = userStatsService.updateStatsValue(userId, statsType, value); + if (!updated) { + return Result.error("更新失败"); + } + return Result.success(); + } + + /** + * 增加用户统计值 + */ + @PutMapping("/user/{userId}/type/{statsType}/increment") + public Result incrementStatsValue(@PathVariable String userId, @PathVariable String statsType, @RequestParam Double increment) { + boolean updated = userStatsService.incrementStatsValue(userId, statsType, increment); + if (!updated) { + return Result.error("增加失败"); + } + return Result.success(); + } + + /** + * 重新计算用户统计 + */ + @PutMapping("/user/{userId}/recalculate") + public Result recalculateUserStats(@PathVariable String userId) { + boolean recalculated = userStatsService.recalculateUserStats(userId); + if (!recalculated) { + return Result.error("重新计算失败"); + } + return Result.success(); + } + + /** + * 重新计算所有用户统计 + */ + @PutMapping("/recalculate-all") + public Result recalculateAllUserStats() { + boolean recalculated = userStatsService.recalculateAllUserStats(); + if (!recalculated) { + return Result.error("重新计算失败"); + } + return Result.success(); + } + + /** + * 查询平均统计值 + */ + @GetMapping("/type/{statsType}/avg") + public Result getAvgValueByStatsType(@PathVariable String statsType) { + Double avgValue = userStatsService.getAvgValueByStatsType(statsType); + return Result.success(avgValue); + } + + /** + * 查询最大统计值 + */ + @GetMapping("/type/{statsType}/max") + public Result getMaxValueByStatsType(@PathVariable String statsType) { + Double maxValue = userStatsService.getMaxValueByStatsType(statsType); + return Result.success(maxValue); + } + + /** + * 查询最小统计值 + */ + @GetMapping("/type/{statsType}/min") + public Result getMinValueByStatsType(@PathVariable String statsType) { + Double minValue = userStatsService.getMinValueByStatsType(statsType); + return Result.success(minValue); + } + + /** + * 删除过期的统计数据 + */ + @DeleteMapping("/expired") + public Result deleteExpiredStats(@RequestParam(defaultValue = "30") Integer days) { + boolean deleted = userStatsService.deleteExpiredStats(days); + if (!deleted) { + return Result.error("删除失败"); + } + return Result.success(); + } + + /** + * 转换为响应对象 + */ + 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; + } + + /** + * 用户统计创建请求 + */ + @lombok.Data + public static class UserStatsCreateRequest { + @NotBlank(message = "用户ID不能为空") + private String userId; + + @NotBlank(message = "统计类型不能为空") + private String statsType; + + @NotNull(message = "统计值不能为空") + private Double value; + + private String period; + } + + /** + * 用户统计响应类 + */ + @lombok.Data + @lombok.EqualsAndHashCode(callSuper = true) + public static class UserStatsResponse extends BaseResponse { + private String userId; + private String statsType; + private Double value; + private String period; + } +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/BaseRequest.java b/backend-single/src/main/java/com/emotion/dto/request/BaseRequest.java new file mode 100644 index 0000000..0159f03 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/BaseRequest.java @@ -0,0 +1,23 @@ +package com.emotion.dto.request; + +import lombok.Data; + +/** + * 基础请求类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class BaseRequest { + + /** + * 请求ID,用于链路追踪 + */ + private String requestId; + + /** + * 客户端时间戳 + */ + private Long timestamp; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/IdRequest.java b/backend-single/src/main/java/com/emotion/dto/request/IdRequest.java new file mode 100644 index 0000000..84cc707 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/IdRequest.java @@ -0,0 +1,23 @@ +package com.emotion.dto.request; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * ID请求 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IdRequest extends BaseRequest { + + /** + * ID + */ + @NotBlank(message = "ID不能为空") + private String id; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java b/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java new file mode 100644 index 0000000..a865465 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java @@ -0,0 +1,41 @@ +package com.emotion.dto.request; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * 登录请求 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginRequest extends BaseRequest { + + /** + * 账号 + */ + @NotBlank(message = "账号不能为空") + private String account; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String password; + + /** + * 验证码 + */ + @NotBlank(message = "验证码不能为空") + private String captcha; + + /** + * 验证码key + */ + @NotBlank(message = "验证码key不能为空") + private String captchaKey; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/PageRequest.java b/backend-single/src/main/java/com/emotion/dto/request/PageRequest.java new file mode 100644 index 0000000..698f5d9 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/PageRequest.java @@ -0,0 +1,20 @@ +package com.emotion.dto.request; + +import com.emotion.common.BasePageRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 分页请求 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PageRequest extends BasePageRequest { + + /** + * 额外的查询参数可以在这里扩展 + */ +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java b/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java new file mode 100644 index 0000000..bb091e9 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java @@ -0,0 +1,68 @@ +package com.emotion.dto.request; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * 注册请求 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RegisterRequest extends BaseRequest { + + /** + * 账号 + */ + @NotBlank(message = "账号不能为空") + @Size(min = 3, max = 20, message = "账号长度必须在3-20个字符之间") + private String account; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 邮箱 + */ + @Email(message = "邮箱格式不正确") + private String email; + + /** + * 手机号 + */ + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + /** + * 验证码 + */ + @NotBlank(message = "验证码不能为空") + private String captcha; + + /** + * 验证码key + */ + @NotBlank(message = "验证码key不能为空") + private String captchaKey; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java new file mode 100644 index 0000000..bd3db32 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java @@ -0,0 +1,53 @@ +package com.emotion.dto.request; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * 用户创建请求 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserCreateRequest extends BaseRequest { + + /** + * 账号 + */ + @NotBlank(message = "账号不能为空") + @Size(min = 3, max = 20, message = "账号长度必须在3-20个字符之间") + private String account; + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间") + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; + + /** + * 邮箱 + */ + @Email(message = "邮箱格式不正确") + private String email; + + /** + * 手机号 + */ + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/AuthResponse.java b/backend-single/src/main/java/com/emotion/dto/response/AuthResponse.java new file mode 100644 index 0000000..9f2c0d3 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/AuthResponse.java @@ -0,0 +1,38 @@ +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 认证响应 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class AuthResponse { + + /** + * 访问令牌 + */ + private String accessToken; + + /** + * 刷新令牌 + */ + private String refreshToken; + + /** + * 令牌过期时间(秒) + */ + private Long expiresIn; + + /** + * 用户信息 + */ + private UserInfoResponse userInfo; + + /** + * 登录时间 + */ + private String loginTime; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/BaseResponse.java b/backend-single/src/main/java/com/emotion/dto/response/BaseResponse.java new file mode 100644 index 0000000..899a21c --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/BaseResponse.java @@ -0,0 +1,28 @@ +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 基础响应类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class BaseResponse { + + /** + * 响应ID + */ + private String id; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 更新时间 + */ + private String updateTime; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/CaptchaResponse.java b/backend-single/src/main/java/com/emotion/dto/response/CaptchaResponse.java new file mode 100644 index 0000000..8f15d93 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/CaptchaResponse.java @@ -0,0 +1,28 @@ +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 验证码响应 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class CaptchaResponse { + + /** + * 验证码key + */ + private String captchaKey; + + /** + * 验证码图片(Base64编码) + */ + private String captchaImage; + + /** + * 过期时间(秒) + */ + private Long expiresIn; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java b/backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java new file mode 100644 index 0000000..f604cce --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java @@ -0,0 +1,73 @@ +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 用户信息响应 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +public class UserInfoResponse { + + /** + * 用户ID + */ + private String id; + + /** + * 账号 + */ + private String account; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 头像 + */ + private String avatar; + + /** + * 状态 + */ + private Integer status; + + /** + * 会员等级 + */ + private String memberLevel; + + /** + * 使用天数 + */ + private Integer totalDays; + + /** + * 最后活跃时间 + */ + private String lastActiveTime; + + /** + * 创建时间 + */ + private String createTime; +} diff --git a/backend-single/src/main/java/com/emotion/dto/response/UserResponse.java b/backend-single/src/main/java/com/emotion/dto/response/UserResponse.java new file mode 100644 index 0000000..97041dd --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/UserResponse.java @@ -0,0 +1,60 @@ +package com.emotion.dto.response; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户响应 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserResponse extends BaseResponse { + + /** + * 账号 + */ + private String account; + + /** + * 用户名 + */ + private String username; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 头像 + */ + private String avatar; + + /** + * 状态 + */ + private Integer status; + + /** + * 会员等级 + */ + private String memberLevel; + + /** + * 使用天数 + */ + private Integer totalDays; + + /** + * 最后活跃时间 + */ + private String lastActiveTime; +} diff --git a/backend-single/src/main/java/com/emotion/exception/AuthException.java b/backend-single/src/main/java/com/emotion/exception/AuthException.java new file mode 100644 index 0000000..1cbee68 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/exception/AuthException.java @@ -0,0 +1,18 @@ +package com.emotion.exception; + +/** + * 认证异常类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public class AuthException extends BusinessException { + + public AuthException(String message) { + super(401, message); + } + + public AuthException(String message, Throwable cause) { + super(401, message, cause); + } +} diff --git a/backend-single/src/main/java/com/emotion/exception/BusinessException.java b/backend-single/src/main/java/com/emotion/exception/BusinessException.java new file mode 100644 index 0000000..184f480 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/exception/BusinessException.java @@ -0,0 +1,40 @@ +package com.emotion.exception; + +/** + * 业务异常 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public class BusinessException extends RuntimeException { + + private Integer code; + + public BusinessException(String message) { + super(message); + this.code = 500; + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = 500; + } + + public BusinessException(Integer code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } +} diff --git a/backend-single/src/main/java/com/emotion/exception/CaptchaException.java b/backend-single/src/main/java/com/emotion/exception/CaptchaException.java new file mode 100644 index 0000000..44db258 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/exception/CaptchaException.java @@ -0,0 +1,18 @@ +package com.emotion.exception; + +/** + * 验证码异常类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public class CaptchaException extends BusinessException { + + public CaptchaException(String message) { + super(400, message); + } + + public CaptchaException(String message, Throwable cause) { + super(400, message, cause); + } +} diff --git a/backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java b/backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4aaea46 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java @@ -0,0 +1,165 @@ +package com.emotion.exception; + +import com.emotion.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.Set; + +/** + * 全局异常处理器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 处理认证异常 + */ + @ExceptionHandler(AuthException.class) + public Result handleAuthException(AuthException e, HttpServletRequest request) { + log.warn("认证异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage()); + return Result.unauthorized(e.getMessage()); + } + + /** + * 处理令牌异常 + */ + @ExceptionHandler(TokenException.class) + public Result handleTokenException(TokenException e, HttpServletRequest request) { + log.warn("令牌异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage()); + return Result.unauthorized(e.getMessage()); + } + + /** + * 处理验证码异常 + */ + @ExceptionHandler(CaptchaException.class) + public Result handleCaptchaException(CaptchaException e, HttpServletRequest request) { + log.warn("验证码异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage()); + return Result.badRequest(e.getMessage()); + } + + /** + * 处理业务异常 + */ + @ExceptionHandler(BusinessException.class) + public Result handleBusinessException(BusinessException e, HttpServletRequest request) { + log.warn("业务异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage()); + + if (e.getCode() == 401) { + return Result.unauthorized(e.getMessage()); + } else if (e.getCode() == 403) { + return Result.forbidden(e.getMessage()); + } else if (e.getCode() == 400) { + return Result.badRequest(e.getMessage()); + } else { + return Result.error(e.getMessage()); + } + } + + /** + * 处理参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { + log.warn("参数校验失败: {} {}", request.getMethod(), request.getRequestURI(), e); + + StringBuilder message = new StringBuilder("参数校验失败: "); + for (FieldError error : e.getBindingResult().getFieldErrors()) { + message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; "); + } + + return Result.badRequest(message.toString()); + } + + /** + * 处理Bean校验异常 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e, HttpServletRequest request) { + log.warn("参数绑定失败: {} {}", request.getMethod(), request.getRequestURI(), e); + + StringBuilder message = new StringBuilder("参数绑定失败: "); + for (FieldError error : e.getBindingResult().getFieldErrors()) { + message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; "); + } + + return Result.badRequest(message.toString()); + } + + /** + * 处理约束校验异常 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) { + log.warn("约束校验失败: {} {}", request.getMethod(), request.getRequestURI(), e); + + StringBuilder message = new StringBuilder("约束校验失败: "); + Set> violations = e.getConstraintViolations(); + for (ConstraintViolation violation : violations) { + message.append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("; "); + } + + return Result.badRequest(message.toString()); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) { + log.warn("非法参数: {} {}", request.getMethod(), request.getRequestURI(), e); + return Result.badRequest("参数错误: " + e.getMessage()); + } + + /** + * 处理空指针异常 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e, HttpServletRequest request) { + log.error("空指针异常: {} {}", request.getMethod(), request.getRequestURI(), e); + return Result.error("系统内部错误"); + } + + /** + * 处理运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e, HttpServletRequest request) { + log.error("运行时异常: {} {}", request.getMethod(), request.getRequestURI(), e); + return Result.error("系统运行异常: " + e.getMessage()); + } + + /** + * 处理所有其他异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e, HttpServletRequest request) { + log.error("未知异常: {} {}", request.getMethod(), request.getRequestURI(), e); + return Result.error("系统异常,请联系管理员"); + } + + +} diff --git a/backend-single/src/main/java/com/emotion/exception/TokenException.java b/backend-single/src/main/java/com/emotion/exception/TokenException.java new file mode 100644 index 0000000..1f57e86 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/exception/TokenException.java @@ -0,0 +1,18 @@ +package com.emotion.exception; + +/** + * 令牌异常类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public class TokenException extends AuthException { + + public TokenException(String message) { + super(message); + } + + public TokenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..5ca6b2f --- /dev/null +++ b/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java @@ -0,0 +1,113 @@ +package com.emotion.interceptor; + +import com.emotion.service.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 认证拦截器 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Component +public class AuthInterceptor implements HandlerInterceptor { + + @Autowired + private AuthService authService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 跨域预检请求直接放行 + if ("OPTIONS".equals(request.getMethod())) { + return true; + } + + // 获取请求路径 + String requestURI = request.getRequestURI(); + + // 白名单路径,不需要认证 + if (isWhiteList(requestURI)) { + return true; + } + + // 提取访问令牌 + String token = extractToken(request); + if (token == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未提供访问令牌\",\"data\":null}"); + return false; + } + + // 验证访问令牌 + if (!authService.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"访问令牌无效或已过期\",\"data\":null}"); + return false; + } + + // 将用户ID存储到请求属性中,供后续使用 + String userId = authService.getUserIdFromToken(token); + request.setAttribute("userId", userId); + request.setAttribute("token", token); + + return true; + } + + /** + * 检查是否为白名单路径 + */ + private boolean isWhiteList(String requestURI) { + // 认证相关接口 + if (requestURI.startsWith("/auth/")) { + return true; + } + + // 静态资源 + if (requestURI.startsWith("/static/") || + requestURI.startsWith("/css/") || + requestURI.startsWith("/js/") || + requestURI.startsWith("/images/")) { + return true; + } + + // Swagger文档 + if (requestURI.startsWith("/swagger-") || + requestURI.startsWith("/v2/api-docs") || + requestURI.startsWith("/webjars/")) { + return true; + } + + // 健康检查 + if (requestURI.equals("/health") || requestURI.equals("/actuator/health")) { + return true; + } + + return false; + } + + /** + * 从请求中提取访问令牌 + */ + private String extractToken(HttpServletRequest request) { + // 从Authorization头中获取 + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + + // 从请求参数中获取 + String tokenParam = request.getParameter("token"); + if (tokenParam != null && !tokenParam.trim().isEmpty()) { + return tokenParam.trim(); + } + + return null; + } +} diff --git a/backend-single/src/main/java/com/emotion/mapper/RewardMapper.java b/backend-single/src/main/java/com/emotion/mapper/RewardMapper.java deleted file mode 100644 index 2b3e297c7207888efb09429e7b16fcec55f6a9e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 616 zcmZvZ!D_-l5QhJApzq*WP>ZE_Xp!LEi$cL;8&h30F(ig6#mDIb^qKk|ZNJ&pb`xaT zncdl$fByOR>$A|K5^b<6rRs3rDN~j?4;oELCP*cj_3hV@^O zWs{i-YtP07Z0#*)nG!z}m>R~u%`VbFT~>q^UG~G`eHP}tB_H$i+u6rp)*!FF5*@mS zn)kSwQYkk3!;P85nAl$pb { + + /** + * 分页查询成就 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据分类查询成就 + */ + List getByCategory(String category); + + /** + * 根据稀有度查询成就 + */ + List getByRarity(String rarity); + + /** + * 根据条件类型查询成就 + */ + List getByConditionType(String conditionType); + + /** + * 查询已解锁的成就 + */ + List getUnlockedAchievements(); + + /** + * 查询未解锁的成就 + */ + List getLockedAchievements(); + + /** + * 查询隐藏的成就 + */ + List getHiddenAchievements(); + + /** + * 查询可见的成就 + */ + List getVisibleAchievements(); + + /** + * 根据进度范围查询成就 + */ + List getByProgressRange(Double minProgress, Double maxProgress); + + /** + * 根据解锁时间范围查询成就 + */ + List getByUnlockTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计已解锁成就数量 + */ + Long countUnlockedAchievements(); + + /** + * 统计未解锁成就数量 + */ + Long countLockedAchievements(); + + /** + * 统计指定分类的成就数量 + */ + Long countByCategory(String category); + + /** + * 统计指定稀有度的成就数量 + */ + Long countByRarity(String rarity); + + /** + * 查询平均进度 + */ + Double getAvgProgress(); + + /** + * 查询指定分类的平均进度 + */ + Double getAvgProgressByCategory(String category); + + /** + * 查询最近解锁的成就 + */ + List getRecentlyUnlocked(Integer limit); + + /** + * 查询即将完成的成就(进度>80%) + */ + List getNearCompletion(); + + /** + * 查询稀有成就(稀有度为legendary或epic) + */ + List getRareAchievements(); + + /** + * 更新成就解锁状态 + */ + boolean unlockAchievement(String id, LocalDateTime unlockedTime); + + /** + * 更新成就进度 + */ + boolean updateProgress(String id, Double progress); + + /** + * 更新成就隐藏状态 + */ + boolean updateHiddenStatus(String id, Integer isHidden); + + /** + * 查询推荐成就(基于分类和稀有度) + */ + List getRecommendedAchievements(String category, String rarity, Integer limit); +} diff --git a/backend-single/src/main/java/com/emotion/service/AuthService.java b/backend-single/src/main/java/com/emotion/service/AuthService.java new file mode 100644 index 0000000..66659d6 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/AuthService.java @@ -0,0 +1,105 @@ +package com.emotion.service; + +import com.emotion.dto.request.LoginRequest; +import com.emotion.dto.request.RegisterRequest; +import com.emotion.dto.response.AuthResponse; +import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.UserInfoResponse; + +/** + * 认证服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface AuthService { + + /** + * 用户登录 + * + * @param request 登录请求 + * @return 认证响应 + */ + AuthResponse login(LoginRequest request); + + /** + * 用户注册 + * + * @param request 注册请求 + * @return 认证响应 + */ + 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 token 访问令牌 + * @return 是否登出成功 + */ + boolean logoutByToken(String token); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 新的认证响应 + */ + AuthResponse refreshToken(String refreshToken); + + /** + * 验证访问令牌 + * + * @param token 访问令牌 + * @return 是否有效 + */ + boolean validateToken(String token); + + /** + * 从令牌中获取用户ID + * + * @param token 访问令牌 + * @return 用户ID + */ + String getUserIdFromToken(String token); + + /** + * 从令牌中获取用户名 + * + * @param token 访问令牌 + * @return 用户名 + */ + String getUsernameFromToken(String token); +} diff --git a/backend-single/src/main/java/com/emotion/service/CommentService.java b/backend-single/src/main/java/com/emotion/service/CommentService.java new file mode 100644 index 0000000..2dfb773 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/CommentService.java @@ -0,0 +1,128 @@ +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.entity.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 评论服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface CommentService extends IService { + + /** + * 分页查询评论 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据帖子ID分页查询评论 + */ + IPage getPageByPostId(BasePageRequest request, String postId); + + /** + * 根据用户ID分页查询评论 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据帖子ID查询所有评论 + */ + List getByPostId(String postId); + + /** + * 根据用户ID查询所有评论 + */ + List getByUserId(String userId); + + /** + * 根据回复的评论ID查询回复 + */ + List getRepliesByCommentId(String replyToId); + + /** + * 查询顶级评论(非回复的评论) + */ + List getTopLevelCommentsByPostId(String postId); + + /** + * 根据点赞数范围查询评论 + */ + List getByLikesRange(Integer minLikes, Integer maxLikes); + + /** + * 根据时间范围查询评论 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计帖子的评论数量 + */ + Long countByPostId(String postId); + + /** + * 统计用户的评论数量 + */ + Long countByUserId(String userId); + + /** + * 统计评论的回复数量 + */ + Long countRepliesByCommentId(String commentId); + + /** + * 统计帖子的顶级评论数量 + */ + Long countTopLevelCommentsByPostId(String postId); + + /** + * 查询最受欢迎的评论(按点赞数排序) + */ + List getMostLikedCommentsByPostId(String postId, Integer limit); + + /** + * 查询最新的评论 + */ + List getLatestCommentsByPostId(String postId, Integer limit); + + /** + * 查询用户最近的评论 + */ + List getRecentByUserId(String userId, Integer limit); + + /** + * 查询热门评论(按点赞数和回复数综合排序) + */ + List getPopularCommentsByPostId(String postId, Integer limit); + + /** + * 根据关键词搜索评论内容 + */ + List searchByKeyword(String keyword); + + /** + * 根据帖子ID和关键词搜索评论 + */ + List searchByPostIdAndKeyword(String postId, String keyword); + + /** + * 查询用户在指定帖子下的评论 + */ + List getByPostIdAndUserId(String postId, String userId); + + /** + * 更新评论点赞数 + */ + boolean updateLikes(String id, Integer increment); + + /** + * 创建评论 + */ + Comment createComment(String postId, String userId, String content, String replyToId); +} diff --git a/backend-single/src/main/java/com/emotion/service/CommunityPostService.java b/backend-single/src/main/java/com/emotion/service/CommunityPostService.java new file mode 100644 index 0000000..2ba5d76 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/CommunityPostService.java @@ -0,0 +1,159 @@ +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.entity.CommunityPost; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 社区帖子服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface CommunityPostService extends IService { + + /** + * 分页查询帖子 + */ + IPage getPage(BasePageRequest request); + + /** + * 分页查询公开帖子 + */ + IPage getPublicPostsPage(BasePageRequest request); + + /** + * 根据用户ID分页查询帖子 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据地点ID查询帖子 + */ + List getByLocationId(String locationId); + + /** + * 根据帖子类型查询帖子 + */ + List getByType(String type); + + /** + * 查询用户的私密帖子 + */ + List getPrivatePostsByUserId(String userId); + + /** + * 根据点赞数范围查询帖子 + */ + List getByLikesRange(Integer minLikes, Integer maxLikes); + + /** + * 根据浏览数范围查询帖子 + */ + List getByViewRange(Integer minViews, Integer maxViews); + + /** + * 根据评论数范围查询帖子 + */ + List getByCommentRange(Integer minComments, Integer maxComments); + + /** + * 根据时间范围查询帖子 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计用户的帖子数量 + */ + Long countByUserId(String userId); + + /** + * 统计用户的公开帖子数量 + */ + Long countPublicPostsByUserId(String userId); + + /** + * 统计用户的私密帖子数量 + */ + Long countPrivatePostsByUserId(String userId); + + /** + * 统计指定类型的帖子数量 + */ + Long countByType(String type); + + /** + * 统计地点的帖子数量 + */ + Long countByLocationId(String locationId); + + /** + * 查询最受欢迎的帖子(按点赞数排序) + */ + List getMostLikedPosts(Integer limit); + + /** + * 查询最热门的帖子(按浏览数排序) + */ + List getMostViewedPosts(Integer limit); + + /** + * 查询最新的帖子 + */ + List getLatestPosts(Integer limit); + + /** + * 查询热门帖子(综合点赞、浏览、评论) + */ + List getPopularPosts(Integer limit); + + /** + * 根据标签搜索帖子 + */ + List getByTag(String tag); + + /** + * 根据关键词搜索帖子 + */ + List searchByKeyword(String keyword); + + /** + * 查询用户最近的帖子 + */ + List getRecentByUserId(String userId, Integer limit); + + /** + * 更新帖子点赞数 + */ + boolean updateLikes(String id, Integer increment); + + /** + * 更新帖子浏览数 + */ + boolean incrementViewCount(String id); + + /** + * 更新帖子评论数 + */ + boolean updateCommentCount(String id, Integer increment); + + /** + * 更新帖子隐私状态 + */ + boolean updatePrivacyStatus(String id, Integer isPrivate); + + /** + * 查询推荐帖子(基于类型和地点) + */ + List getRecommendedPosts(String type, String locationId, Integer limit); + + /** + * 创建帖子 + */ + CommunityPost createPost(String userId, String title, String content, String type, + String locationId, String tags, Integer isPrivate); +} diff --git a/backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java b/backend-single/src/main/java/com/emotion/service/CozeApiCallService.java similarity index 86% rename from backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java rename to backend-single/src/main/java/com/emotion/service/CozeApiCallService.java index 6188dd5..a88181c 100644 --- a/backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java +++ b/backend-single/src/main/java/com/emotion/service/CozeApiCallService.java @@ -1,8 +1,8 @@ package com.emotion.service; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; +import com.emotion.common.BasePageRequest; import com.emotion.entity.CozeApiCall; import java.math.BigDecimal; @@ -15,17 +15,22 @@ import java.util.List; * @author emotion-museum * @date 2025-07-23 */ -public interface ICozeApiCallService extends IService { +public interface CozeApiCallService extends IService { + + /** + * 分页查询API调用记录 + */ + IPage getPage(BasePageRequest request); /** * 根据会话ID分页查询API调用记录 */ - IPage getByConversationId(Page page, String conversationId); + IPage getPageByConversationId(BasePageRequest request, String conversationId); /** * 根据用户ID分页查询API调用记录 */ - IPage getByUserId(Page page, String userId); + IPage getPageByUserId(BasePageRequest request, String userId); /** * 根据Bot ID查询API调用记录 diff --git a/backend-single/src/main/java/com/emotion/service/EmotionAnalysisService.java b/backend-single/src/main/java/com/emotion/service/EmotionAnalysisService.java new file mode 100644 index 0000000..91deb6c --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/EmotionAnalysisService.java @@ -0,0 +1,99 @@ +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.entity.EmotionAnalysis; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 情绪分析服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface EmotionAnalysisService extends IService { + + /** + * 分页查询情绪分析记录 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据用户ID分页查询情绪分析记录 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据消息ID查询情绪分析记录 + */ + EmotionAnalysis getByMessageId(String messageId); + + /** + * 根据主要情绪查询分析记录 + */ + List getByPrimaryEmotion(String primaryEmotion); + + /** + * 根据情绪极性查询分析记录 + */ + List getByPolarity(String polarity); + + /** + * 根据用户ID和情绪类型查询分析记录 + */ + List getByUserIdAndEmotion(String userId, String primaryEmotion); + + /** + * 根据时间范围查询情绪分析记录 + */ + List getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计用户的情绪分析记录数量 + */ + Long countByUserId(String userId); + + /** + * 统计指定情绪类型的记录数量 + */ + Long countByPrimaryEmotion(String primaryEmotion); + + /** + * 统计用户指定情绪类型的记录数量 + */ + Long countByUserIdAndEmotion(String userId, String primaryEmotion); + + /** + * 查询用户最近的情绪分析记录 + */ + List getRecentByUserId(String userId, Integer limit); + + /** + * 查询高置信度的情绪分析记录 + */ + List getByMinConfidence(Double minConfidence); + + /** + * 查询用户的平均情绪强度 + */ + Double getAvgIntensityByUserId(String userId); + + /** + * 查询用户指定时间段的平均情绪强度 + */ + Double getAvgIntensityByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 查询用户最常见的情绪类型 + */ + String getMostFrequentEmotionByUserId(String userId); + + /** + * 创建情绪分析记录 + */ + EmotionAnalysis createEmotionAnalysis(String messageId, String userId, String primaryEmotion, + String polarity, Double intensity, Double confidence); +} diff --git a/backend-single/src/main/java/com/emotion/service/EmotionRecordService.java b/backend-single/src/main/java/com/emotion/service/EmotionRecordService.java new file mode 100644 index 0000000..35f4202 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/EmotionRecordService.java @@ -0,0 +1,109 @@ +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.entity.EmotionRecord; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 情绪记录服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface EmotionRecordService extends IService { + + /** + * 分页查询情绪记录 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据用户ID分页查询情绪记录 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据用户ID查询情绪记录 + */ + List getByUserId(String userId); + + /** + * 根据情绪类型查询记录 + */ + List getByEmotionType(String emotionType); + + /** + * 根据用户ID和情绪类型查询记录 + */ + List getByUserIdAndEmotionType(String userId, String emotionType); + + /** + * 根据时间范围查询情绪记录 + */ + List getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据强度范围查询情绪记录 + */ + List getByIntensityRange(Double minIntensity, Double maxIntensity); + + /** + * 统计用户的情绪记录数量 + */ + Long countByUserId(String userId); + + /** + * 统计指定情绪类型的记录数量 + */ + Long countByEmotionType(String emotionType); + + /** + * 统计用户指定情绪类型的记录数量 + */ + Long countByUserIdAndEmotionType(String userId, String emotionType); + + /** + * 查询用户最近的情绪记录 + */ + List getRecentByUserId(String userId, Integer limit); + + /** + * 查询用户的平均情绪强度 + */ + Double getAvgIntensityByUserId(String userId); + + /** + * 查询用户指定时间段的平均情绪强度 + */ + Double getAvgIntensityByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 查询用户最常见的情绪类型 + */ + String getMostFrequentEmotionByUserId(String userId); + + /** + * 查询高强度情绪记录 + */ + List getHighIntensityRecords(Double minIntensity); + + /** + * 根据触发因素查询情绪记录 + */ + List getByTrigger(String trigger); + + /** + * 根据地点查询情绪记录 + */ + List getByLocation(String location); + + /** + * 创建情绪记录 + */ + EmotionRecord createEmotionRecord(String userId, String emotionType, Double intensity, + String trigger, String location, String notes); +} diff --git a/backend-single/src/main/java/com/emotion/service/GrowthTopicService.java b/backend-single/src/main/java/com/emotion/service/GrowthTopicService.java new file mode 100644 index 0000000..155653c --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/GrowthTopicService.java @@ -0,0 +1,124 @@ +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.entity.GrowthTopic; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 成长话题服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface GrowthTopicService extends IService { + + /** + * 分页查询成长话题 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据分类查询话题 + */ + List getByCategory(String category); + + /** + * 根据难度等级查询话题 + */ + List getByDifficultyLevel(String difficultyLevel); + + /** + * 根据状态查询话题 + */ + List getByStatus(String status); + + /** + * 查询推荐话题 + */ + List getRecommendedTopics(Integer limit); + + /** + * 查询热门话题 + */ + List getPopularTopics(Integer limit); + + /** + * 查询最新话题 + */ + List getLatestTopics(Integer limit); + + /** + * 根据参与人数范围查询话题 + */ + List getByParticipantRange(Integer minParticipants, Integer maxParticipants); + + /** + * 根据时间范围查询话题 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计指定分类的话题数量 + */ + Long countByCategory(String category); + + /** + * 统计指定状态的话题数量 + */ + Long countByStatus(String status); + + /** + * 统计指定难度等级的话题数量 + */ + Long countByDifficultyLevel(String difficultyLevel); + + /** + * 查询平均参与人数 + */ + Double getAvgParticipantCount(); + + /** + * 查询指定分类的平均参与人数 + */ + Double getAvgParticipantCountByCategory(String category); + + /** + * 根据标签搜索话题 + */ + List searchByTags(String tags); + + /** + * 根据关键词搜索话题 + */ + List searchByKeyword(String keyword); + + /** + * 更新话题参与人数 + */ + boolean updateParticipantCount(String id, Integer increment); + + /** + * 更新话题状态 + */ + boolean updateStatus(String id, String status); + + /** + * 查询即将结束的话题 + */ + List getEndingSoonTopics(Integer days); + + /** + * 查询长期话题 + */ + List getLongTermTopics(); + + /** + * 创建成长话题 + */ + GrowthTopic createGrowthTopic(String title, String description, String category, + String difficultyLevel, String tags, LocalDateTime endTime); +} diff --git a/backend-single/src/main/java/com/emotion/service/GuestUserService.java b/backend-single/src/main/java/com/emotion/service/GuestUserService.java new file mode 100644 index 0000000..fee79c8 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/GuestUserService.java @@ -0,0 +1,123 @@ +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.entity.GuestUser; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 访客用户服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface GuestUserService extends IService { + + /** + * 分页查询访客用户 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据设备ID查询访客用户 + */ + GuestUser getByDeviceId(String deviceId); + + /** + * 根据IP地址查询访客用户 + */ + List getByIpAddress(String ipAddress); + + /** + * 根据用户代理查询访客用户 + */ + List getByUserAgent(String userAgent); + + /** + * 根据状态查询访客用户 + */ + List getByStatus(String status); + + /** + * 根据时间范围查询访客用户 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据最后活跃时间范围查询访客用户 + */ + List getByLastActiveTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计指定状态的访客用户数量 + */ + Long countByStatus(String status); + + /** + * 统计指定IP地址的访客用户数量 + */ + Long countByIpAddress(String ipAddress); + + /** + * 统计今日新增访客用户 + */ + Long countTodayNewGuests(); + + /** + * 统计活跃访客用户 + */ + Long countActiveGuests(Integer days); + + /** + * 查询最近访问的访客用户 + */ + List getRecentVisitors(Integer limit); + + /** + * 查询长时间未活跃的访客用户 + */ + List getInactiveGuests(Integer days); + + /** + * 根据访问次数范围查询访客用户 + */ + List 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); +} diff --git a/backend-single/src/main/java/com/emotion/service/IAiService.java b/backend-single/src/main/java/com/emotion/service/IAiService.java deleted file mode 100644 index 0764a84..0000000 --- a/backend-single/src/main/java/com/emotion/service/IAiService.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.emotion.service; - -import com.emotion.entity.Message; - -import java.util.List; - -/** - * AI服务接口 - * - * @author emotion-museum - * @date 2025-07-23 - */ -public interface IAiService { - - /** - * 发送聊天消息到AI - * - * @param conversationId 会话ID - * @param message 用户消息 - * @param userId 用户ID - * @return AI回复内容 - */ - String sendChatMessage(String conversationId, String message, String userId); - - /** - * 根据聊天记录生成对话总结 - * - * @param conversationId 会话ID - * @param userId 用户ID - * @return 对话总结 - */ - String generateConversationSummary(String conversationId, String userId); - - /** - * 根据消息列表生成总结 - * - * @param messages 消息列表 - * @param userId 用户ID - * @return 对话总结 - */ - String generateSummaryFromRecords(List messages, String userId); - - /** - * 检查AI服务是否可用 - * - * @return 是否可用 - */ - boolean isServiceAvailable(); - - /** - * 获取AI服务状态信息 - * - * @return 状态信息 - */ - String getServiceStatus(); -} diff --git a/backend-single/src/main/java/com/emotion/service/IConversationService.java b/backend-single/src/main/java/com/emotion/service/IConversationService.java deleted file mode 100644 index 68b1bbb..0000000 --- a/backend-single/src/main/java/com/emotion/service/IConversationService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.emotion.service; - -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.IService; -import com.emotion.entity.Conversation; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 会话服务接口 - * - * @author emotion-museum - * @date 2025-07-23 - */ -public interface IConversationService extends IService { - - /** - * 创建新会话 - * - * @param userId 用户ID - * @param title 会话标题 - * @param type 会话类型 - * @return 会话信息 - */ - Conversation createConversation(String userId, String title, String type); - - /** - * 根据用户ID分页查询会话列表 - * - * @param page 分页参数 - * @param userId 用户ID - * @return 会话分页数据 - */ - IPage getByUserId(Page page, String userId); - - /** - * 根据用户ID查询活跃会话列表 - * - * @param userId 用户ID - * @return 活跃会话列表 - */ - List getActiveByUserId(String userId); - - /** - * 根据Coze会话ID查询会话 - * - * @param cozeConversationId Coze会话ID - * @return 会话信息 - */ - Conversation getByCozeConversationId(String cozeConversationId); - - /** - * 更新会话消息数量 - * - * @param conversationId 会话ID - * @param messageCount 消息数量 - * @return 是否成功 - */ - boolean updateMessageCount(String conversationId, Integer messageCount); - - /** - * 更新会话状态 - * - * @param conversationId 会话ID - * @param status 状态 - * @return 是否成功 - */ - boolean updateStatus(String conversationId, Integer status); - - /** - * 结束会话 - * - * @param conversationId 会话ID - * @return 是否成功 - */ - boolean endConversation(String conversationId); - - /** - * 统计用户的会话数量 - * - * @param userId 用户ID - * @return 会话数量 - */ - Long countByUserId(String userId); - - /** - * 统计用户的活跃会话数量 - * - * @param userId 用户ID - * @return 活跃会话数量 - */ - Long countActiveByUserId(String userId); - - /** - * 归档超时会话 - * - * @param days 超时天数 - * @return 归档的会话数量 - */ - int archiveTimeoutConversations(Integer days); - - /** - * 批量归档会话 - * - * @param conversationIds 会话ID列表 - * @return 是否成功 - */ - boolean batchArchive(List conversationIds); - - /** - * 获取或创建会话 - * - * @param userId 用户ID - * @param cozeConversationId Coze会话ID - * @param title 会话标题 - * @return 会话信息 - */ - Conversation getOrCreateConversation(String userId, String cozeConversationId, String title); -} diff --git a/backend-single/src/main/java/com/emotion/service/IMessageService.java b/backend-single/src/main/java/com/emotion/service/IMessageService.java deleted file mode 100644 index 42331c7..0000000 --- a/backend-single/src/main/java/com/emotion/service/IMessageService.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.emotion.service; - -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.IService; -import com.emotion.entity.Message; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 消息服务接口 - * - * @author emotion-museum - * @date 2025-07-23 - */ -public interface IMessageService extends IService { - - /** - * 保存消息 - * - * @param conversationId 会话ID - * @param content 消息内容 - * @param type 消息类型 - * @param sender 发送者 - * @return 消息 - */ - Message saveMessage(String conversationId, String content, String type, String sender); - - /** - * 根据会话ID分页查询消息 - * - * @param page 分页参数 - * @param conversationId 会话ID - * @return 消息分页数据 - */ - IPage getByConversationId(Page page, String conversationId); - - /** - * 根据发送者分页查询消息 - * - * @param page 分页参数 - * @param sender 发送者 - * @return 消息分页数据 - */ - IPage getBySender(Page page, String sender); - - /** - * 根据会话ID查询消息列表(用于总结) - * - * @param conversationId 会话ID - * @param limit 限制数量 - * @return 消息列表 - */ - List getByConversationIdForSummary(String conversationId, Integer limit); - - /** - * 根据时间范围查询消息 - * - * @param conversationId 会话ID - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 消息列表 - */ - List getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime); - - /** - * 统计会话的消息数量 - * - * @param conversationId 会话ID - * @return 消息数量 - */ - Long countByConversationId(String conversationId); - - /** - * 统计发送者的消息数量 - * - * @param sender 发送者 - * @return 消息数量 - */ - Long countBySender(String sender); - - /** - * 查询会话的最后一条消息 - * - * @param conversationId 会话ID - * @return 最后一条消息 - */ - Message getLastMessageByConversationId(String conversationId); - - /** - * 根据发送者查询消息 - * - * @param conversationId 会话ID - * @param sender 发送者 - * @return 消息列表 - */ - List getByConversationIdAndSender(String conversationId, String sender); - - /** - * 更新消息状态 - * - * @param messageId 消息ID - * @param status 状态 - * @return 是否成功 - */ - boolean updateStatus(String messageId, String status); - - /** - * 更新消息已读状态 - * - * @param messageId 消息ID - * @param isRead 是否已读 - * @return 是否成功 - */ - boolean updateReadStatus(String messageId, Integer isRead); - - /** - * 批量更新会话消息为已读 - * - * @param conversationId 会话ID - * @return 是否成功 - */ - boolean markConversationMessagesAsRead(String conversationId); - - /** - * 批量保存消息 - * - * @param messages 消息列表 - * @return 是否成功 - */ - boolean saveBatch(List messages); - - /** - * 删除会话的所有消息 - * - * @param conversationId 会话ID - * @return 是否成功 - */ - boolean deleteByConversationId(String conversationId); -} diff --git a/backend-single/src/main/java/com/emotion/service/LocationPinService.java b/backend-single/src/main/java/com/emotion/service/LocationPinService.java new file mode 100644 index 0000000..e69de29 diff --git a/backend-single/src/main/java/com/emotion/service/RewardService.java b/backend-single/src/main/java/com/emotion/service/RewardService.java new file mode 100644 index 0000000..afd4c65 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/RewardService.java @@ -0,0 +1,144 @@ +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.entity.Reward; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 奖励服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface RewardService extends IService { + + /** + * 分页查询奖励 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据用户ID分页查询奖励 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据用户ID查询奖励 + */ + List getByUserId(String userId); + + /** + * 根据奖励类型查询奖励 + */ + List getByRewardType(String rewardType); + + /** + * 根据用户ID和奖励类型查询奖励 + */ + List getByUserIdAndRewardType(String userId, String rewardType); + + /** + * 根据状态查询奖励 + */ + List getByStatus(String status); + + /** + * 根据用户ID和状态查询奖励 + */ + List getByUserIdAndStatus(String userId, String status); + + /** + * 根据积分范围查询奖励 + */ + List getByPointsRange(Integer minPoints, Integer maxPoints); + + /** + * 根据时间范围查询奖励 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据获得时间范围查询奖励 + */ + List 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 getRecentByUserId(String userId, Integer limit); + + /** + * 查询高积分奖励 + */ + List getHighPointsRewards(Integer minPoints); + + /** + * 查询待领取的奖励 + */ + List getPendingRewardsByUserId(String userId); + + /** + * 查询已领取的奖励 + */ + List getClaimedRewardsByUserId(String userId); + + /** + * 查询已过期的奖励 + */ + List getExpiredRewards(); + + /** + * 更新奖励状态 + */ + boolean updateStatus(String id, String status, LocalDateTime claimedTime); + + /** + * 批量更新过期奖励状态 + */ + boolean updateExpiredRewards(); + + /** + * 根据来源查询奖励 + */ + List getBySource(String source); + + /** + * 创建奖励 + */ + Reward createReward(String userId, String rewardType, Integer points, String source, + String description, LocalDateTime expiredTime); +} diff --git a/backend-single/src/main/java/com/emotion/service/TokenService.java b/backend-single/src/main/java/com/emotion/service/TokenService.java new file mode 100644 index 0000000..060d921 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/TokenService.java @@ -0,0 +1,36 @@ +package com.emotion.service; + +import com.emotion.dto.response.UserInfoResponse; + +/** + * 令牌服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface TokenService { + + /** + * 从请求中提取并验证令牌,获取用户信息 + * + * @param token 访问令牌 + * @return 用户信息响应 + */ + UserInfoResponse getUserInfoByToken(String token); + + /** + * 从请求中提取并验证令牌,获取用户名 + * + * @param token 访问令牌 + * @return 用户名 + */ + String getUsernameByToken(String token); + + /** + * 验证令牌并返回用户ID + * + * @param token 访问令牌 + * @return 用户ID + */ + String validateTokenAndGetUserId(String token); +} diff --git a/backend-single/src/main/java/com/emotion/service/TopicInteractionService.java b/backend-single/src/main/java/com/emotion/service/TopicInteractionService.java new file mode 100644 index 0000000..4c98514 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/TopicInteractionService.java @@ -0,0 +1,149 @@ +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.entity.TopicInteraction; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 话题互动服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface TopicInteractionService extends IService { + + /** + * 分页查询话题互动 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据话题ID分页查询互动 + */ + IPage getPageByTopicId(BasePageRequest request, String topicId); + + /** + * 根据用户ID分页查询互动 + */ + IPage getPageByUserId(BasePageRequest request, String userId); + + /** + * 根据话题ID查询互动 + */ + List getByTopicId(String topicId); + + /** + * 根据用户ID查询互动 + */ + List getByUserId(String userId); + + /** + * 根据互动类型查询互动 + */ + List getByInteractionType(String interactionType); + + /** + * 根据话题ID和互动类型查询互动 + */ + List getByTopicIdAndInteractionType(String topicId, String interactionType); + + /** + * 根据用户ID和互动类型查询互动 + */ + List getByUserIdAndInteractionType(String userId, String interactionType); + + /** + * 根据话题ID和用户ID查询互动 + */ + List getByTopicIdAndUserId(String topicId, String userId); + + /** + * 根据时间范围查询互动 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 统计话题的互动数量 + */ + Long countByTopicId(String topicId); + + /** + * 统计用户的互动数量 + */ + Long countByUserId(String userId); + + /** + * 统计指定类型的互动数量 + */ + Long countByInteractionType(String interactionType); + + /** + * 统计话题指定类型的互动数量 + */ + Long countByTopicIdAndInteractionType(String topicId, String interactionType); + + /** + * 统计用户指定类型的互动数量 + */ + Long countByUserIdAndInteractionType(String userId, String interactionType); + + /** + * 查询话题最近的互动 + */ + List getRecentByTopicId(String topicId, Integer limit); + + /** + * 查询用户最近的互动 + */ + List getRecentByUserId(String userId, Integer limit); + + /** + * 查询热门互动(按点赞数排序) + */ + List getPopularInteractions(Integer limit); + + /** + * 查询话题的热门互动 + */ + List getPopularInteractionsByTopicId(String topicId, Integer limit); + + /** + * 根据点赞数范围查询互动 + */ + List getByLikesRange(Integer minLikes, Integer maxLikes); + + /** + * 查询用户是否已参与话题 + */ + boolean hasUserInteracted(String topicId, String userId); + + /** + * 查询用户在话题中的特定互动类型 + */ + TopicInteraction getUserInteractionByType(String topicId, String userId, String interactionType); + + /** + * 更新互动点赞数 + */ + boolean updateLikes(String id, Integer increment); + + /** + * 根据内容关键词搜索互动 + */ + List searchByContent(String keyword); + + /** + * 根据话题ID和内容关键词搜索互动 + */ + List searchByTopicIdAndContent(String topicId, String keyword); + + /** + * 创建话题互动 + */ + TopicInteraction createTopicInteraction(String topicId, String userId, String interactionType, + String content, String attachments); +} diff --git a/backend-single/src/main/java/com/emotion/service/UserStatsService.java b/backend-single/src/main/java/com/emotion/service/UserStatsService.java new file mode 100644 index 0000000..29a00df --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/UserStatsService.java @@ -0,0 +1,128 @@ +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.entity.UserStats; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户统计服务接口 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public interface UserStatsService extends IService { + + /** + * 分页查询用户统计 + */ + IPage getPage(BasePageRequest request); + + /** + * 根据用户ID查询统计信息 + */ + UserStats getByUserId(String userId); + + /** + * 根据统计类型查询统计信息 + */ + List getByStatsType(String statsType); + + /** + * 根据用户ID和统计类型查询统计信息 + */ + UserStats getByUserIdAndStatsType(String userId, String statsType); + + /** + * 根据时间范围查询统计信息 + */ + List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据数值范围查询统计信息 + */ + List getByValueRange(Double minValue, Double maxValue); + + /** + * 统计指定类型的记录数量 + */ + Long countByStatsType(String statsType); + + /** + * 查询平均统计值 + */ + Double getAvgValueByStatsType(String statsType); + + /** + * 查询最大统计值 + */ + Double getMaxValueByStatsType(String statsType); + + /** + * 查询最小统计值 + */ + Double getMinValueByStatsType(String statsType); + + /** + * 查询用户的所有统计类型 + */ + List getAllStatsByUserId(String userId); + + /** + * 查询排名前N的用户统计 + */ + List getTopUsersByStatsType(String statsType, Integer limit); + + /** + * 查询用户在指定统计类型中的排名 + */ + Long getUserRankByStatsType(String userId, String statsType); + + /** + * 更新用户统计值 + */ + boolean updateStatsValue(String userId, String statsType, Double value); + + /** + * 增加用户统计值 + */ + boolean incrementStatsValue(String userId, String statsType, Double increment); + + /** + * 批量更新用户统计 + */ + boolean batchUpdateStats(String userId, List statsList); + + /** + * 重新计算用户统计 + */ + boolean recalculateUserStats(String userId); + + /** + * 重新计算所有用户统计 + */ + boolean recalculateAllUserStats(); + + /** + * 根据周期查询统计信息 + */ + List getByPeriod(String period); + + /** + * 根据用户ID和周期查询统计信息 + */ + List getByUserIdAndPeriod(String userId, String period); + + /** + * 创建或更新用户统计 + */ + UserStats createOrUpdateUserStats(String userId, String statsType, Double value, String period); + + /** + * 删除过期的统计数据 + */ + boolean deleteExpiredStats(Integer days); +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/AchievementServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AchievementServiceImpl.java new file mode 100644 index 0000000..386c581 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/AchievementServiceImpl.java @@ -0,0 +1,259 @@ +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.entity.Achievement; +import com.emotion.mapper.AchievementMapper; +import com.emotion.service.AchievementService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 成就服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class AchievementServiceImpl extends ServiceImpl implements AchievementService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(Achievement::getTitle, request.getKeyword()) + .or().like(Achievement::getDescription, request.getKeyword())); + } + + 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 getByCategory(String category) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getCategory, category) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByRarity(String rarity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getRarity, rarity) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByConditionType(String conditionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getConditionType, conditionType) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getUnlockedAchievements() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(Achievement::getUnlockedTime) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getUnlockedTime); + return this.list(wrapper); + } + + @Override + public List getLockedAchievements() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNull(Achievement::getUnlockedTime) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getHiddenAchievements() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getIsHidden, 1) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getVisibleAchievements() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getIsHidden, 0) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByProgressRange(Double minProgress, Double maxProgress) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Achievement::getProgress, minProgress, maxProgress) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getProgress); + return this.list(wrapper); + } + + @Override + public List getByUnlockTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(Achievement::getUnlockedTime) + .eq(Achievement::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countLockedAchievements() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNull(Achievement::getUnlockedTime) + .eq(Achievement::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByCategory(String category) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getCategory, category) + .eq(Achievement::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByRarity(String rarity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Achievement::getRarity, rarity) + .eq(Achievement::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Double getAvgProgress() { + List achievements = this.list(new LambdaQueryWrapper() + .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 achievements = this.list(new LambdaQueryWrapper() + .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 getRecentlyUnlocked(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(Achievement::getUnlockedTime) + .eq(Achievement::getIsDeleted, 0) + .orderByDesc(Achievement::getUnlockedTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getNearCompletion() { + LambdaQueryWrapper 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 getRareAchievements() { + LambdaQueryWrapper 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 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 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 wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Achievement::getId, id) + .set(Achievement::getIsHidden, isHidden) + .set(Achievement::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public List getRecommendedAchievements(String category, String rarity, Integer limit) { + LambdaQueryWrapper 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); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..212ecd7 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java @@ -0,0 +1,405 @@ +package com.emotion.service.impl; + +import com.emotion.dto.request.LoginRequest; +import com.emotion.dto.request.RegisterRequest; +import com.emotion.dto.response.AuthResponse; +import com.emotion.dto.response.CaptchaResponse; +import com.emotion.dto.response.UserInfoResponse; +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 org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import javax.imageio.ImageIO; +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 emotion-museum + * @date 2025-07-23 + */ +@Service +public class AuthServiceImpl implements AuthService { + + @Autowired + private UserService userService; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String CAPTCHA_PREFIX = "captcha:"; + 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 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 AuthResponse login(LoginRequest request) { + // 验证验证码 + if (!validateCaptcha(request.getCaptchaKey(), request.getCaptcha())) { + throw new CaptchaException("验证码错误或已过期"); + } + + // 根据账号查询用户 + User user = userService.getByAccount(request.getAccount()); + if (user == null) { + throw new AuthException("账号不存在"); + } + + // 验证密码(这里应该使用加密后的密码比较) + if (!verifyPassword(request.getPassword(), user.getPassword())) { + throw new AuthException("密码错误"); + } + + // 检查用户状态 + 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 (!validateCaptcha(request.getCaptchaKey(), request.getCaptcha())) { + throw new CaptchaException("验证码错误或已过期"); + } + + // 检查账号是否已存在 + if (userService.getByAccount(request.getAccount()) != null) { + throw new BusinessException("账号已存在"); + } + + // 检查邮箱是否已存在 + if (StringUtils.hasText(request.getEmail()) && userService.getByEmail(request.getEmail()) != null) { + throw new BusinessException("邮箱已被使用"); + } + + // 创建用户 + User user = userService.createUser( + request.getAccount(), + StringUtils.hasText(request.getUsername()) ? request.getUsername() : request.getAccount(), + encryptPassword(request.getPassword()), + request.getEmail(), + request.getPhone() + ); + + // 设置昵称 + if (StringUtils.hasText(request.getNickname())) { + user.setNickname(request.getNickname()); + userService.updateById(user); + } + + // 生成令牌 + 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)); + + return response; + } + + @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(String token) { + String userId = validateTokenAndGetUserId(token); + return logout(userId, token); + } + + /** + * 验证令牌并获取用户ID + */ + private String validateTokenAndGetUserId(String token) { + if (!StringUtils.hasText(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(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + return redisTemplate.hasKey(TOKEN_PREFIX + token); + } + + @Override + public String getUserIdFromToken(String token) { + if (!StringUtils.hasText(token)) { + return null; + } + return (String) redisTemplate.opsForValue().get(TOKEN_PREFIX + token); + } + + @Override + public String getUsernameFromToken(String token) { + String userId = getUserIdFromToken(token); + if (userId == null) { + return null; + } + User user = userService.getById(userId); + return user != null ? user.getUsername() : null; + } + + /** + * 生成访问令牌 + */ + private String generateAccessToken(User user) { + String token = UUID.randomUUID().toString().replace("-", ""); + redisTemplate.opsForValue().set( + TOKEN_PREFIX + token, + user.getId(), + TOKEN_EXPIRE_HOURS, + TimeUnit.HOURS + ); + return token; + } + + /** + * 生成刷新令牌 + */ + private String generateRefreshToken(User user) { + String refreshToken = UUID.randomUUID().toString().replace("-", ""); + 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 rawPassword.equals(encodedPassword); + } + + /** + * 加密密码 + */ + private String encryptPassword(String rawPassword) { + // 这里应该使用BCrypt等加密算法进行密码加密 + // 简化实现,实际项目中应该使用加密算法 + return 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; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java new file mode 100644 index 0000000..5f3fb7a --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java @@ -0,0 +1,261 @@ +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.entity.Comment; +import com.emotion.mapper.CommentMapper; +import com.emotion.service.CommentService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 评论服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class CommentServiceImpl extends ServiceImpl implements CommentService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(Comment::getContent, request.getKeyword()); + } + + wrapper.eq(Comment::getIsDeleted, 0); + + // 排序 + if (StringUtils.hasText(request.getOrderBy())) { + if ("asc".equalsIgnoreCase(request.getOrderDirection())) { + wrapper.orderByAsc(Comment::getCreateTime); + } else { + wrapper.orderByDesc(Comment::getCreateTime); + } + } else { + wrapper.orderByDesc(Comment::getCreateTime); + } + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByPostId(BasePageRequest request, String postId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(Comment::getContent, request.getKeyword()); + } + + wrapper.orderByAsc(Comment::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getUserId, userId) + .eq(Comment::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(Comment::getContent, request.getKeyword()); + } + + wrapper.orderByDesc(Comment::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByPostId(String postId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getIsDeleted, 0) + .orderByAsc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getUserId, userId) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getRepliesByCommentId(String replyToId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getReplyToId, replyToId) + .eq(Comment::getIsDeleted, 0) + .orderByAsc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getTopLevelCommentsByPostId(String postId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .isNull(Comment::getReplyToId) + .eq(Comment::getIsDeleted, 0) + .orderByAsc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByLikesRange(Integer minLikes, Integer maxLikes) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Comment::getLikes, minLikes, maxLikes) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getLikes); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Comment::getCreateTime, startTime, endTime) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByPostId(String postId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getUserId, userId) + .eq(Comment::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countRepliesByCommentId(String commentId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getReplyToId, commentId) + .eq(Comment::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countTopLevelCommentsByPostId(String postId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .isNull(Comment::getReplyToId) + .eq(Comment::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getMostLikedCommentsByPostId(String postId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getLikes) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getLatestCommentsByPostId(String postId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getUserId, userId) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getPopularCommentsByPostId(String postId, Integer limit) { + // 简化版本,按点赞数排序 + return getMostLikedCommentsByPostId(postId, limit); + } + + @Override + public List searchByKeyword(String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(Comment::getContent, keyword) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List searchByPostIdAndKeyword(String postId, String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .like(Comment::getContent, keyword) + .eq(Comment::getIsDeleted, 0) + .orderByDesc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByPostIdAndUserId(String postId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Comment::getPostId, postId) + .eq(Comment::getUserId, userId) + .eq(Comment::getIsDeleted, 0) + .orderByAsc(Comment::getCreateTime); + return this.list(wrapper); + } + + @Override + public boolean updateLikes(String id, Integer increment) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Comment::getId, id) + .setSql("likes = likes + " + increment) + .set(Comment::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public Comment createComment(String postId, String userId, String content, String replyToId) { + Comment comment = Comment.builder() + .id(UUID.randomUUID().toString()) + .postId(postId) + .userId(userId) + .content(content) + .replyToId(replyToId) + .likes(0) + .build(); + this.save(comment); + return comment; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java new file mode 100644 index 0000000..3c588b1 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java @@ -0,0 +1,323 @@ +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.entity.CommunityPost; +import com.emotion.mapper.CommunityPostMapper; +import com.emotion.service.CommunityPostService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 社区帖子服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class CommunityPostServiceImpl extends ServiceImpl implements CommunityPostService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + 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); + + return this.page(page, wrapper); + } + + @Override + public IPage getPublicPostsPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(CommunityPost::getTitle, request.getKeyword()) + .or().like(CommunityPost::getContent, request.getKeyword())); + } + + wrapper.eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsDeleted, 0); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(CommunityPost::getTitle, request.getKeyword()) + .or().like(CommunityPost::getContent, request.getKeyword())); + } + + wrapper.orderByDesc(CommunityPost::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByLocationId(String locationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getLocationId, locationId) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByType(String type) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getType, type) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getPrivatePostsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsPrivate, 1) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByLikesRange(Integer minLikes, Integer maxLikes) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(CommunityPost::getLikes, minLikes, maxLikes) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getLikes); + return this.list(wrapper); + } + + @Override + public List getByViewRange(Integer minViews, Integer maxViews) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(CommunityPost::getViewCount, minViews, maxViews) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getViewCount); + return this.list(wrapper); + } + + @Override + public List getByCommentRange(Integer minComments, Integer maxComments) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(CommunityPost::getCommentCount, minComments, maxComments) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCommentCount); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(CommunityPost::getCreateTime, startTime, endTime) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countPublicPostsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countPrivatePostsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsPrivate, 1) + .eq(CommunityPost::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByType(String type) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getType, type) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByLocationId(String locationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getLocationId, locationId) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getMostLikedPosts(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getLikes) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getMostViewedPosts(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getViewCount) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getLatestPosts(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getPopularPosts(Integer limit) { + // 简化版本,按点赞数排序 + return getMostLikedPosts(limit); + } + + @Override + public List getByTag(String tag) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(CommunityPost::getTags, tag) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public List searchByKeyword(String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.and(w -> w.like(CommunityPost::getTitle, keyword) + .or().like(CommunityPost::getContent, keyword)) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getUserId, userId) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public boolean updateLikes(String id, Integer increment) { + // 使用原生SQL更新 + return this.update() + .setSql("likes = likes + " + increment) + .eq("id", id) + .update(); + } + + @Override + public boolean incrementViewCount(String id) { + return this.update() + .setSql("view_count = view_count + 1") + .eq("id", id) + .update(); + } + + @Override + public boolean updateCommentCount(String id, Integer increment) { + return this.update() + .setSql("comment_count = comment_count + " + increment) + .eq("id", id) + .update(); + } + + @Override + public boolean updatePrivacyStatus(String id, Integer isPrivate) { + return this.update() + .set("is_private", isPrivate) + .eq("id", id) + .update(); + } + + @Override + public List getRecommendedPosts(String type, String locationId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CommunityPost::getType, type) + .eq(CommunityPost::getLocationId, locationId) + .eq(CommunityPost::getIsPrivate, 0) + .eq(CommunityPost::getIsDeleted, 0) + .orderByDesc(CommunityPost::getLikes) + .orderByDesc(CommunityPost::getViewCount) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public CommunityPost createPost(String userId, String title, String content, String type, + String locationId, String tags, Integer isPrivate) { + CommunityPost post = CommunityPost.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .title(title) + .content(content) + .type(type) + .locationId(locationId) + .tags(tags) + .isPrivate(isPrivate != null ? isPrivate : 0) + .likes(0) + .viewCount(0) + .commentCount(0) + .build(); + this.save(post); + return post; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/ConversationServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/ConversationServiceImpl.java new file mode 100644 index 0000000..c64fa66 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/ConversationServiceImpl.java @@ -0,0 +1,229 @@ +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.entity.Conversation; +import com.emotion.mapper.ConversationMapper; +import com.emotion.service.ConversationService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 对话服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class ConversationServiceImpl extends ServiceImpl implements ConversationService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(Conversation::getTitle, request.getKeyword()); + } + + wrapper.eq(Conversation::getIsDeleted, 0); + + // 排序 + if (StringUtils.hasText(request.getOrderBy())) { + if ("asc".equalsIgnoreCase(request.getOrderDirection())) { + wrapper.orderByAsc(Conversation::getCreateTime); + } else { + wrapper.orderByDesc(Conversation::getCreateTime); + } + } else { + wrapper.orderByDesc(Conversation::getCreateTime); + } + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(Conversation::getTitle, request.getKeyword()); + } + + wrapper.orderByDesc(Conversation::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByType(String type) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getType, type) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getStatus, status) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndType(String userId, String type) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getType, type) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndStatus(String userId, String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getStatus, status) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Conversation::getCreateTime, startTime, endTime) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByType(String type) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getType, type) + .eq(Conversation::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getStatus, status) + .eq(Conversation::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getUserId, userId) + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getActiveConversations() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getStatus, "active") + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getLastMessageTime); + return this.list(wrapper); + } + + @Override + public List getArchivedConversations() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Conversation::getStatus, "archived") + .eq(Conversation::getIsDeleted, 0) + .orderByDesc(Conversation::getCreateTime); + return this.list(wrapper); + } + + @Override + public boolean updateMessageCount(String id, Integer increment) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Conversation::getId, id) + .setSql("message_count = message_count + " + increment) + .set(Conversation::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean updateLastMessageTime(String id, LocalDateTime lastMessageTime) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Conversation::getId, id) + .set(Conversation::getLastMessageTime, lastMessageTime) + .set(Conversation::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean updateStatus(String id, String status) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Conversation::getId, id) + .set(Conversation::getStatus, status) + .set(Conversation::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean archiveConversation(String id) { + return updateStatus(id, "archived"); + } + + @Override + public boolean activateConversation(String id) { + return updateStatus(id, "active"); + } + + @Override + public Conversation createConversation(String userId, String title, String type, String clientIp) { + Conversation conversation = Conversation.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .title(title) + .type(type) + .status("active") + .messageCount(0) + .clientIp(clientIp) + .build(); + this.save(conversation); + return conversation; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java index 6fe19e6..5bdc2b8 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java @@ -5,10 +5,12 @@ 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.entity.CozeApiCall; import com.emotion.mapper.CozeApiCallMapper; -import com.emotion.service.ICozeApiCallService; +import com.emotion.service.CozeApiCallService; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -16,28 +18,71 @@ import java.util.List; /** * Coze API调用记录服务实现类 - * + * * @author emotion-museum * @date 2025-07-23 */ @Service -public class CozeApiCallServiceImpl extends ServiceImpl implements ICozeApiCallService { +public class CozeApiCallServiceImpl extends ServiceImpl implements CozeApiCallService { @Override - public IPage getByConversationId(Page page, String conversationId) { + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CozeApiCall::getConversationId, conversationId) - .eq(CozeApiCall::getIsDeleted, 0) - .orderByDesc(CozeApiCall::getStartTime); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(CozeApiCall::getRequestUrl, request.getKeyword()) + .or().like(CozeApiCall::getAiReply, 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); + } + return this.page(page, wrapper); } @Override - public IPage getByUserId(Page page, String userId) { + public IPage getPageByConversationId(BasePageRequest request, String conversationId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getConversationId, conversationId) + .eq(CozeApiCall::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(CozeApiCall::getRequestUrl, request.getKeyword()) + .or().like(CozeApiCall::getAiReply, request.getKeyword()); + } + + wrapper.orderByDesc(CozeApiCall::getStartTime); + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(CozeApiCall::getUserId, userId) - .eq(CozeApiCall::getIsDeleted, 0) - .orderByDesc(CozeApiCall::getStartTime); + .eq(CozeApiCall::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(CozeApiCall::getRequestUrl, request.getKeyword()) + .or().like(CozeApiCall::getAiReply, request.getKeyword()); + } + + wrapper.orderByDesc(CozeApiCall::getStartTime); return this.page(page, wrapper); } diff --git a/backend-single/src/main/java/com/emotion/service/impl/EmotionAnalysisServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/EmotionAnalysisServiceImpl.java new file mode 100644 index 0000000..945d4f8 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/EmotionAnalysisServiceImpl.java @@ -0,0 +1,218 @@ +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.entity.EmotionAnalysis; +import com.emotion.mapper.EmotionAnalysisMapper; +import com.emotion.service.EmotionAnalysisService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 情绪分析服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class EmotionAnalysisServiceImpl extends ServiceImpl implements EmotionAnalysisService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.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 getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(EmotionAnalysis::getPrimaryEmotion, request.getKeyword()) + .or().like(EmotionAnalysis::getPolarity, request.getKeyword()); + } + + wrapper.orderByDesc(EmotionAnalysis::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public EmotionAnalysis getByMessageId(String messageId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getMessageId, messageId) + .eq(EmotionAnalysis::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public List getByPrimaryEmotion(String primaryEmotion) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getPrimaryEmotion, primaryEmotion) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByPolarity(String polarity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getPolarity, polarity) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndEmotion(String userId, String primaryEmotion) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getPrimaryEmotion, primaryEmotion) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .between(EmotionAnalysis::getCreateTime, startTime, endTime) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByPrimaryEmotion(String primaryEmotion) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getPrimaryEmotion, primaryEmotion) + .eq(EmotionAnalysis::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserIdAndEmotion(String userId, String primaryEmotion) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getPrimaryEmotion, primaryEmotion) + .eq(EmotionAnalysis::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getByMinConfidence(Double minConfidence) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(EmotionAnalysis::getConfidence, minConfidence) + .eq(EmotionAnalysis::getIsDeleted, 0) + .orderByDesc(EmotionAnalysis::getConfidence); + return this.list(wrapper); + } + + @Override + public Double getAvgIntensityByUserId(String userId) { + List analyses = this.list(new LambdaQueryWrapper() + .eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getIsDeleted, 0) + .isNotNull(EmotionAnalysis::getIntensity)); + return analyses.stream() + .mapToDouble(a -> a.getIntensity() != null ? a.getIntensity().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public Double getAvgIntensityByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) { + List analyses = this.list(new LambdaQueryWrapper() + .eq(EmotionAnalysis::getUserId, userId) + .between(EmotionAnalysis::getCreateTime, startTime, endTime) + .eq(EmotionAnalysis::getIsDeleted, 0) + .isNotNull(EmotionAnalysis::getIntensity)); + return analyses.stream() + .mapToDouble(a -> a.getIntensity() != null ? a.getIntensity().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public String getMostFrequentEmotionByUserId(String userId) { + // 简化实现,实际应该使用GROUP BY查询 + List analyses = this.list(new LambdaQueryWrapper() + .eq(EmotionAnalysis::getUserId, userId) + .eq(EmotionAnalysis::getIsDeleted, 0)); + + return analyses.stream() + .collect(java.util.stream.Collectors.groupingBy( + EmotionAnalysis::getPrimaryEmotion, + java.util.stream.Collectors.counting())) + .entrySet().stream() + .max(java.util.Map.Entry.comparingByValue()) + .map(java.util.Map.Entry::getKey) + .orElse("unknown"); + } + + @Override + public EmotionAnalysis createEmotionAnalysis(String messageId, String userId, String primaryEmotion, + String polarity, Double intensity, Double confidence) { + EmotionAnalysis analysis = EmotionAnalysis.builder() + .id(UUID.randomUUID().toString()) + .messageId(messageId) + .userId(userId) + .primaryEmotion(primaryEmotion) + .polarity(polarity) + .intensity(intensity) + .confidence(confidence) + .build(); + this.save(analysis); + return analysis; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/EmotionRecordServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/EmotionRecordServiceImpl.java new file mode 100644 index 0000000..f541300 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/EmotionRecordServiceImpl.java @@ -0,0 +1,238 @@ +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.entity.EmotionRecord; +import com.emotion.mapper.EmotionRecordMapper; +import com.emotion.service.EmotionRecordService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 情绪记录服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class EmotionRecordServiceImpl extends ServiceImpl implements EmotionRecordService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(EmotionRecord::getEmotionType, request.getKeyword()) + .or().like(EmotionRecord::getTrigger, request.getKeyword()) + .or().like(EmotionRecord::getNotes, request.getKeyword())); + } + + wrapper.eq(EmotionRecord::getIsDeleted, 0); + + // 排序 + if (StringUtils.hasText(request.getOrderBy())) { + if ("asc".equalsIgnoreCase(request.getOrderDirection())) { + wrapper.orderByAsc(EmotionRecord::getCreateTime); + } else { + wrapper.orderByDesc(EmotionRecord::getCreateTime); + } + } else { + wrapper.orderByDesc(EmotionRecord::getCreateTime); + } + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(EmotionRecord::getEmotionType, request.getKeyword()) + .or().like(EmotionRecord::getTrigger, request.getKeyword()) + .or().like(EmotionRecord::getNotes, request.getKeyword())); + } + + wrapper.orderByDesc(EmotionRecord::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByEmotionType(String emotionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getEmotionType, emotionType) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndEmotionType(String userId, String emotionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getEmotionType, emotionType) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .between(EmotionRecord::getCreateTime, startTime, endTime) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByIntensityRange(Double minIntensity, Double maxIntensity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(EmotionRecord::getIntensity, minIntensity, maxIntensity) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getIntensity); + return this.list(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByEmotionType(String emotionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getEmotionType, emotionType) + .eq(EmotionRecord::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserIdAndEmotionType(String userId, String emotionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getEmotionType, emotionType) + .eq(EmotionRecord::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public Double getAvgIntensityByUserId(String userId) { + List records = this.list(new LambdaQueryWrapper() + .eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0) + .isNotNull(EmotionRecord::getIntensity)); + return records.stream() + .mapToDouble(r -> r.getIntensity() != null ? r.getIntensity().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public Double getAvgIntensityByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) { + List records = this.list(new LambdaQueryWrapper() + .eq(EmotionRecord::getUserId, userId) + .between(EmotionRecord::getCreateTime, startTime, endTime) + .eq(EmotionRecord::getIsDeleted, 0) + .isNotNull(EmotionRecord::getIntensity)); + return records.stream() + .mapToDouble(r -> r.getIntensity() != null ? r.getIntensity().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public String getMostFrequentEmotionByUserId(String userId) { + List records = this.list(new LambdaQueryWrapper() + .eq(EmotionRecord::getUserId, userId) + .eq(EmotionRecord::getIsDeleted, 0)); + + return records.stream() + .collect(java.util.stream.Collectors.groupingBy( + EmotionRecord::getEmotionType, + java.util.stream.Collectors.counting())) + .entrySet().stream() + .max(java.util.Map.Entry.comparingByValue()) + .map(java.util.Map.Entry::getKey) + .orElse("unknown"); + } + + @Override + public List getHighIntensityRecords(Double minIntensity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(EmotionRecord::getIntensity, minIntensity) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getIntensity); + return this.list(wrapper); + } + + @Override + public List getByTrigger(String trigger) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(EmotionRecord::getTrigger, trigger) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByLocation(String location) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(EmotionRecord::getLocation, location) + .eq(EmotionRecord::getIsDeleted, 0) + .orderByDesc(EmotionRecord::getCreateTime); + return this.list(wrapper); + } + + @Override + public EmotionRecord createEmotionRecord(String userId, String emotionType, Double intensity, + String trigger, String location, String notes) { + EmotionRecord record = EmotionRecord.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .emotionType(emotionType) + .intensity(intensity) + .trigger(trigger) + .location(location) + .notes(notes) + .build(); + this.save(record); + return record; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/GrowthTopicServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/GrowthTopicServiceImpl.java new file mode 100644 index 0000000..e12667f --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/GrowthTopicServiceImpl.java @@ -0,0 +1,251 @@ +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.entity.GrowthTopic; +import com.emotion.mapper.GrowthTopicMapper; +import com.emotion.service.GrowthTopicService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 成长话题服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class GrowthTopicServiceImpl extends ServiceImpl implements GrowthTopicService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(GrowthTopic::getTitle, request.getKeyword()) + .or().like(GrowthTopic::getDescription, request.getKeyword()) + .or().like(GrowthTopic::getTags, 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 List getByCategory(String category) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getCategory, category) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByDifficultyLevel(String difficultyLevel) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getDifficultyLevel, difficultyLevel) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getStatus, status) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getRecommendedTopics(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getStatus, "active") + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getParticipantCount) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getPopularTopics(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getParticipantCount) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getLatestTopics(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getByParticipantRange(Integer minParticipants, Integer maxParticipants) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(GrowthTopic::getParticipantCount, minParticipants, maxParticipants) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getParticipantCount); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(GrowthTopic::getCreateTime, startTime, endTime) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByCategory(String category) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getCategory, category) + .eq(GrowthTopic::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getStatus, status) + .eq(GrowthTopic::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByDifficultyLevel(String difficultyLevel) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GrowthTopic::getDifficultyLevel, difficultyLevel) + .eq(GrowthTopic::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Double getAvgParticipantCount() { + List topics = this.list(new LambdaQueryWrapper() + .eq(GrowthTopic::getIsDeleted, 0) + .isNotNull(GrowthTopic::getParticipantCount)); + return topics.stream() + .mapToDouble(t -> t.getParticipantCount() != null ? t.getParticipantCount().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public Double getAvgParticipantCountByCategory(String category) { + List topics = this.list(new LambdaQueryWrapper() + .eq(GrowthTopic::getCategory, category) + .eq(GrowthTopic::getIsDeleted, 0) + .isNotNull(GrowthTopic::getParticipantCount)); + return topics.stream() + .mapToDouble(t -> t.getParticipantCount() != null ? t.getParticipantCount().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public List searchByTags(String tags) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(GrowthTopic::getTags, tags) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public List searchByKeyword(String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.and(w -> w.like(GrowthTopic::getTitle, keyword) + .or().like(GrowthTopic::getDescription, keyword) + .or().like(GrowthTopic::getTags, keyword)) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public boolean updateParticipantCount(String id, Integer increment) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(GrowthTopic::getId, id) + .setSql("participant_count = participant_count + " + increment) + .set(GrowthTopic::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean updateStatus(String id, String status) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(GrowthTopic::getId, id) + .set(GrowthTopic::getStatus, status) + .set(GrowthTopic::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public List getEndingSoonTopics(Integer days) { + LocalDateTime endTime = LocalDateTime.now().plusDays(days); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.le(GrowthTopic::getEndTime, endTime) + .eq(GrowthTopic::getStatus, "active") + .eq(GrowthTopic::getIsDeleted, 0) + .orderByAsc(GrowthTopic::getEndTime); + return this.list(wrapper); + } + + @Override + public List getLongTermTopics() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNull(GrowthTopic::getEndTime) + .eq(GrowthTopic::getIsDeleted, 0) + .orderByDesc(GrowthTopic::getCreateTime); + return this.list(wrapper); + } + + @Override + public GrowthTopic createGrowthTopic(String title, String description, String category, + String difficultyLevel, String tags, LocalDateTime endTime) { + GrowthTopic topic = GrowthTopic.builder() + .id(UUID.randomUUID().toString()) + .title(title) + .description(description) + .category(category) + .difficultyLevel(difficultyLevel) + .tags(tags) + .endTime(endTime) + .status("active") + .participantCount(0) + .build(); + this.save(topic); + return topic; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/GuestUserServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/GuestUserServiceImpl.java new file mode 100644 index 0000000..866b569 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/GuestUserServiceImpl.java @@ -0,0 +1,233 @@ +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.entity.GuestUser; +import com.emotion.mapper.GuestUserMapper; +import com.emotion.service.GuestUserService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 访客用户服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class GuestUserServiceImpl extends ServiceImpl implements GuestUserService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(GuestUser::getDeviceId, request.getKeyword()) + .or().like(GuestUser::getIpAddress, request.getKeyword())); + } + + wrapper.eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public GuestUser getByDeviceId(String deviceId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getDeviceId, deviceId) + .eq(GuestUser::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public List getByIpAddress(String ipAddress) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getIpAddress, ipAddress) + .eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserAgent(String userAgent) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(GuestUser::getUserAgent, userAgent) + .eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getStatus, status) + .eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(GuestUser::getCreateTime, startTime, endTime) + .eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByLastActiveTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getStatus, status) + .eq(GuestUser::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByIpAddress(String ipAddress) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getIpAddress, ipAddress) + .eq(GuestUser::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countTodayNewGuests() { + LocalDateTime startOfDay = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(GuestUser::getCreateTime, startOfDay) + .eq(GuestUser::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countActiveGuests(Integer days) { + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(days); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(GuestUser::getLastActiveTime, cutoffTime) + .eq(GuestUser::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getRecentVisitors(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getLastActiveTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getInactiveGuests(Integer days) { + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(days); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.lt(GuestUser::getLastActiveTime, cutoffTime) + .eq(GuestUser::getIsDeleted, 0) + .orderByAsc(GuestUser::getLastActiveTime); + return this.list(wrapper); + } + + @Override + public List getByVisitCountRange(Integer minVisits, Integer maxVisits) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(GuestUser::getVisitCount, minVisits, maxVisits) + .eq(GuestUser::getIsDeleted, 0) + .orderByDesc(GuestUser::getVisitCount); + return this.list(wrapper); + } + + @Override + public Double getAvgVisitCount() { + List guests = this.list(new LambdaQueryWrapper() + .eq(GuestUser::getIsDeleted, 0) + .isNotNull(GuestUser::getVisitCount)); + return guests.stream() + .mapToDouble(g -> g.getVisitCount() != null ? g.getVisitCount().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public boolean updateLastActiveTime(String id, LocalDateTime lastActiveTime) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(GuestUser::getId, id) + .set(GuestUser::getLastActiveTime, lastActiveTime) + .set(GuestUser::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean incrementVisitCount(String id) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(GuestUser::getId, id) + .setSql("visit_count = visit_count + 1") + .set(GuestUser::getLastActiveTime, LocalDateTime.now()) + .set(GuestUser::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public boolean updateStatus(String id, String status) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(GuestUser::getId, id) + .set(GuestUser::getStatus, status) + .set(GuestUser::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public GuestUser getOrCreateByDeviceInfo(String deviceId, String ipAddress, String userAgent) { + GuestUser existing = getByDeviceId(deviceId); + if (existing != null) { + incrementVisitCount(existing.getId()); + return existing; + } + return createGuestUser(deviceId, ipAddress, userAgent, null); + } + + @Override + public boolean cleanExpiredGuests(Integer days) { + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(days); + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.lt(GuestUser::getLastActiveTime, cutoffTime) + .set(GuestUser::getIsDeleted, 1) + .set(GuestUser::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public GuestUser createGuestUser(String deviceId, String ipAddress, String userAgent, String location) { + GuestUser guest = GuestUser.builder() + .id(UUID.randomUUID().toString()) + .deviceId(deviceId) + .ipAddress(ipAddress) + .userAgent(userAgent) + .location(location) + .status("active") + .visitCount(1) + .lastActiveTime(LocalDateTime.now()) + .build(); + this.save(guest); + return guest; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/LocationPinServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/LocationPinServiceImpl.java new file mode 100644 index 0000000..e6be7f2 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/LocationPinServiceImpl.java @@ -0,0 +1,50 @@ +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.entity.LocationPin; +import com.emotion.mapper.LocationPinMapper; +import com.emotion.service.LocationPinService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * 位置标记服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class LocationPinServiceImpl extends ServiceImpl implements LocationPinService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(LocationPin::getName, request.getKeyword()) + .or().like(LocationPin::getDescription, request.getKeyword()) + .or().like(LocationPin::getAddress, request.getKeyword())); + } + + wrapper.eq(LocationPin::getIsDeleted, 0); + + // 排序 + if (StringUtils.hasText(request.getOrderBy())) { + if ("asc".equalsIgnoreCase(request.getOrderDirection())) { + wrapper.orderByAsc(LocationPin::getCreateTime); + } else { + wrapper.orderByDesc(LocationPin::getCreateTime); + } + } else { + wrapper.orderByDesc(LocationPin::getCreateTime); + } + + return this.page(page, wrapper); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java new file mode 100644 index 0000000..e778d55 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java @@ -0,0 +1,288 @@ +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.entity.Reward; +import com.emotion.mapper.RewardMapper; +import com.emotion.service.RewardService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 奖励服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class RewardServiceImpl extends ServiceImpl implements RewardService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(Reward::getRewardType, request.getKeyword()) + .or().like(Reward::getDescription, request.getKeyword()) + .or().like(Reward::getSource, request.getKeyword())); + } + + wrapper.eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getIsDeleted, 0); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(Reward::getRewardType, request.getKeyword()) + .or().like(Reward::getDescription, request.getKeyword())); + } + + wrapper.orderByDesc(Reward::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByRewardType(String rewardType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getRewardType, rewardType) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndRewardType(String userId, String rewardType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getRewardType, rewardType) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getStatus, status) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndStatus(String userId, String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getStatus, status) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByPointsRange(Integer minPoints, Integer maxPoints) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Reward::getPoints, minPoints, maxPoints) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getPoints); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(Reward::getCreateTime, startTime, endTime) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByEarnedTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByRewardType(String rewardType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getRewardType, rewardType) + .eq(Reward::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserIdAndRewardType(String userId, String rewardType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getRewardType, rewardType) + .eq(Reward::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getStatus, status) + .eq(Reward::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Integer sumPointsByUserId(String userId) { + List rewards = this.list(new LambdaQueryWrapper() + .eq(Reward::getUserId, userId) + .eq(Reward::getIsDeleted, 0) + .isNotNull(Reward::getPoints)); + return rewards.stream() + .mapToInt(r -> r.getPoints() != null ? r.getPoints() : 0) + .sum(); + } + + @Override + public Integer sumPointsByUserIdAndRewardType(String userId, String rewardType) { + List rewards = this.list(new LambdaQueryWrapper() + .eq(Reward::getUserId, userId) + .eq(Reward::getRewardType, rewardType) + .eq(Reward::getIsDeleted, 0) + .isNotNull(Reward::getPoints)); + return rewards.stream() + .mapToInt(r -> r.getPoints() != null ? r.getPoints() : 0) + .sum(); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getHighPointsRewards(Integer minPoints) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(Reward::getPoints, minPoints) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getPoints); + return this.list(wrapper); + } + + @Override + public List getPendingRewardsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getStatus, "pending") + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getClaimedRewardsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getUserId, userId) + .eq(Reward::getStatus, "claimed") + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getClaimedTime); + return this.list(wrapper); + } + + @Override + public List getExpiredRewards() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.lt(Reward::getExpiredTime, LocalDateTime.now()) + .ne(Reward::getStatus, "expired") + .eq(Reward::getIsDeleted, 0) + .orderByAsc(Reward::getExpiredTime); + return this.list(wrapper); + } + + @Override + public boolean updateStatus(String id, String status, LocalDateTime claimedTime) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(Reward::getId, id) + .set(Reward::getStatus, status) + .set(Reward::getUpdateTime, LocalDateTime.now()); + if (claimedTime != null) { + wrapper.set(Reward::getClaimedTime, claimedTime); + } + return this.update(wrapper); + } + + @Override + public boolean updateExpiredRewards() { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.lt(Reward::getExpiredTime, LocalDateTime.now()) + .ne(Reward::getStatus, "expired") + .set(Reward::getStatus, "expired") + .set(Reward::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public List getBySource(String source) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Reward::getSource, source) + .eq(Reward::getIsDeleted, 0) + .orderByDesc(Reward::getCreateTime); + return this.list(wrapper); + } + + @Override + public Reward createReward(String userId, String rewardType, Integer points, String source, + String description, LocalDateTime expiredTime) { + Reward reward = Reward.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .rewardType(rewardType) + .points(points) + .source(source) + .description(description) + .expiredTime(expiredTime) + .status("pending") + .earnedTime(LocalDateTime.now()) + .build(); + this.save(reward); + return reward; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/TokenServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..bc15d66 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/TokenServiceImpl.java @@ -0,0 +1,64 @@ +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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * 令牌服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class TokenServiceImpl implements TokenService { + + @Autowired + private AuthService authService; + + @Override + public UserInfoResponse getUserInfoByToken(String token) { + String userId = validateTokenAndGetUserId(token); + return authService.getCurrentUserInfo(userId); + } + + @Override + public String getUsernameByToken(String token) { + if (!StringUtils.hasText(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(String token) { + if (!StringUtils.hasText(token)) { + throw new TokenException("未提供访问令牌"); + } + + if (!authService.validateToken(token)) { + throw new TokenException("访问令牌无效或已过期"); + } + + String userId = authService.getUserIdFromToken(token); + if (userId == null) { + throw new TokenException("访问令牌无效"); + } + + return userId; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java new file mode 100644 index 0000000..fc33f94 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java @@ -0,0 +1,291 @@ +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.entity.TopicInteraction; +import com.emotion.mapper.TopicInteractionMapper; +import com.emotion.service.TopicInteractionService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 话题互动服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class TopicInteractionServiceImpl extends ServiceImpl implements TopicInteractionService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(TopicInteraction::getContent, request.getKeyword()); + } + + wrapper.eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public IPage getPageByTopicId(BasePageRequest request, String topicId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getIsDeleted, 0); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(TopicInteraction::getContent, request.getKeyword()); + } + + wrapper.orderByDesc(TopicInteraction::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public IPage getPageByUserId(BasePageRequest request, String userId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.like(TopicInteraction::getContent, request.getKeyword()); + } + + wrapper.orderByDesc(TopicInteraction::getCreateTime); + return this.page(page, wrapper); + } + + @Override + public List getByTopicId(String topicId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByInteractionType(String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByTopicIdAndInteractionType(String topicId, String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndInteractionType(String userId, String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByTopicIdAndUserId(String topicId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(TopicInteraction::getCreateTime, startTime, endTime) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public Long countByTopicId(String topicId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByInteractionType(String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByTopicIdAndInteractionType(String topicId, String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByUserIdAndInteractionType(String userId, String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public List getRecentByTopicId(String topicId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getRecentByUserId(String userId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getPopularInteractions(Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getLikes) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getPopularInteractionsByTopicId(String topicId, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getLikes) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public List getByLikesRange(Integer minLikes, Integer maxLikes) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(TopicInteraction::getLikes, minLikes, maxLikes) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getLikes); + return this.list(wrapper); + } + + @Override + public boolean hasUserInteracted(String topicId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getIsDeleted, 0); + return this.count(wrapper) > 0; + } + + @Override + public TopicInteraction getUserInteractionByType(String topicId, String userId, String interactionType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .eq(TopicInteraction::getUserId, userId) + .eq(TopicInteraction::getInteractionType, interactionType) + .eq(TopicInteraction::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public boolean updateLikes(String id, Integer increment) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(TopicInteraction::getId, id) + .setSql("likes = likes + " + increment) + .set(TopicInteraction::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } + + @Override + public List searchByContent(String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(TopicInteraction::getContent, keyword) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public List searchByTopicIdAndContent(String topicId, String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TopicInteraction::getTopicId, topicId) + .like(TopicInteraction::getContent, keyword) + .eq(TopicInteraction::getIsDeleted, 0) + .orderByDesc(TopicInteraction::getCreateTime); + return this.list(wrapper); + } + + @Override + public TopicInteraction createTopicInteraction(String topicId, String userId, String interactionType, + String content, String attachments) { + TopicInteraction interaction = TopicInteraction.builder() + .id(UUID.randomUUID().toString()) + .topicId(topicId) + .userId(userId) + .interactionType(interactionType) + .content(content) + .attachments(attachments) + .likes(0) + .build(); + this.save(interaction); + return interaction; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/UserStatsServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/UserStatsServiceImpl.java new file mode 100644 index 0000000..e67a7f4 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/UserStatsServiceImpl.java @@ -0,0 +1,265 @@ +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.entity.UserStats; +import com.emotion.mapper.UserStatsMapper; +import com.emotion.service.UserStatsService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 用户统计服务实现类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +@Service +public class UserStatsServiceImpl extends ServiceImpl implements UserStatsService { + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(request.getKeyword())) { + wrapper.and(w -> w.like(UserStats::getStatsType, request.getKeyword()) + .or().like(UserStats::getPeriod, request.getKeyword())); + } + + wrapper.eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public UserStats getByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getUserId, userId) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime) + .last("LIMIT 1"); + return this.getOne(wrapper); + } + + @Override + public List getByStatsType(String statsType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getValue); + return this.list(wrapper); + } + + @Override + public UserStats getByUserIdAndStatsType(String userId, String statsType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getUserId, userId) + .eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime) + .last("LIMIT 1"); + return this.getOne(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(UserStats::getCreateTime, startTime, endTime) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByValueRange(Double minValue, Double maxValue) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.between(UserStats::getValue, minValue, maxValue) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getValue); + return this.list(wrapper); + } + + @Override + public Long countByStatsType(String statsType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Double getAvgValueByStatsType(String statsType) { + List stats = this.list(new LambdaQueryWrapper() + .eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .isNotNull(UserStats::getValue)); + return stats.stream() + .mapToDouble(s -> s.getValue() != null ? s.getValue().doubleValue() : 0.0) + .average() + .orElse(0.0); + } + + @Override + public Double getMaxValueByStatsType(String statsType) { + List stats = this.list(new LambdaQueryWrapper() + .eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .isNotNull(UserStats::getValue)); + return stats.stream() + .mapToDouble(s -> s.getValue() != null ? s.getValue().doubleValue() : 0.0) + .max() + .orElse(0.0); + } + + @Override + public Double getMinValueByStatsType(String statsType) { + List stats = this.list(new LambdaQueryWrapper() + .eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .isNotNull(UserStats::getValue)); + return stats.stream() + .mapToDouble(s -> s.getValue() != null ? s.getValue().doubleValue() : 0.0) + .min() + .orElse(0.0); + } + + @Override + public List getAllStatsByUserId(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getUserId, userId) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getTopUsersByStatsType(String statsType, Integer limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getStatsType, statsType) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getValue) + .last("LIMIT " + limit); + return this.list(wrapper); + } + + @Override + public Long getUserRankByStatsType(String userId, String statsType) { + UserStats userStats = getByUserIdAndStatsType(userId, statsType); + if (userStats == null) { + return 0L; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getStatsType, statsType) + .gt(UserStats::getValue, userStats.getValue()) + .eq(UserStats::getIsDeleted, 0); + return this.count(wrapper) + 1; + } + + @Override + public boolean updateStatsValue(String userId, String statsType, Double value) { + UserStats existing = getByUserIdAndStatsType(userId, statsType); + if (existing != null) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(UserStats::getId, existing.getId()) + .set(UserStats::getValue, value) + .set(UserStats::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } else { + return createOrUpdateUserStats(userId, statsType, value, "daily") != null; + } + } + + @Override + public boolean incrementStatsValue(String userId, String statsType, Double increment) { + UserStats existing = getByUserIdAndStatsType(userId, statsType); + if (existing != null) { + Double newValue = (existing.getValue() != null ? existing.getValue() : 0.0) + increment; + return updateStatsValue(userId, statsType, newValue); + } else { + return createOrUpdateUserStats(userId, statsType, increment, "daily") != null; + } + } + + @Override + public boolean batchUpdateStats(String userId, List statsList) { + for (UserStats stats : statsList) { + updateStatsValue(userId, stats.getStatsType(), stats.getValue()); + } + return true; + } + + @Override + public boolean recalculateUserStats(String userId) { + // 这里应该实现重新计算用户统计的逻辑 + // 简化实现,实际应该根据业务需求计算各种统计值 + return true; + } + + @Override + public boolean recalculateAllUserStats() { + // 这里应该实现重新计算所有用户统计的逻辑 + // 简化实现,实际应该批量处理所有用户 + return true; + } + + @Override + public List getByPeriod(String period) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getPeriod, period) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByUserIdAndPeriod(String userId, String period) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserStats::getUserId, userId) + .eq(UserStats::getPeriod, period) + .eq(UserStats::getIsDeleted, 0) + .orderByDesc(UserStats::getCreateTime); + return this.list(wrapper); + } + + @Override + public UserStats createOrUpdateUserStats(String userId, String statsType, Double value, String period) { + UserStats existing = getByUserIdAndStatsType(userId, statsType); + if (existing != null) { + existing.setValue(value); + existing.setUpdateTime(LocalDateTime.now()); + this.updateById(existing); + return existing; + } else { + UserStats stats = UserStats.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .statsType(statsType) + .value(value) + .period(period) + .build(); + this.save(stats); + return stats; + } + } + + @Override + public boolean deleteExpiredStats(Integer days) { + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(days); + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.lt(UserStats::getCreateTime, cutoffTime) + .set(UserStats::getIsDeleted, 1) + .set(UserStats::getUpdateTime, LocalDateTime.now()); + return this.update(wrapper); + } +} diff --git a/backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java b/backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java new file mode 100644 index 0000000..2c4eab6 --- /dev/null +++ b/backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java @@ -0,0 +1,190 @@ +package com.emotion.controller; + +import com.emotion.dto.request.LoginRequest; +import com.emotion.dto.request.RegisterRequest; +import com.emotion.dto.response.AuthResponse; +import com.emotion.dto.response.CaptchaResponse; +import com.emotion.exception.AuthException; +import com.emotion.exception.CaptchaException; +import com.emotion.service.AuthService; +import com.emotion.service.TokenService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * AuthController测试类 + * + * @author emotion-museum + * @date 2025-07-23 + */ +public class AuthControllerTest { + + @Mock + private AuthService authService; + + @InjectMocks + private AuthController authController; + + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(authController).build(); + objectMapper = new ObjectMapper(); + } + + @Test + void testLogin() throws Exception { + // 准备测试数据 + LoginRequest request = new LoginRequest(); + request.setAccount("testuser"); + request.setPassword("password123"); + request.setCaptcha("1234"); + request.setCaptchaKey("test-key"); + + AuthResponse response = new AuthResponse(); + response.setAccessToken("test-access-token"); + response.setRefreshToken("test-refresh-token"); + response.setExpiresIn(86400L); + + // Mock服务方法 + when(authService.login(any(LoginRequest.class))).thenReturn(response); + + // 执行测试 + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("登录成功")) + .andExpect(jsonPath("$.data.accessToken").value("test-access-token")); + } + + @Test + void testRegister() throws Exception { + // 准备测试数据 + RegisterRequest request = new RegisterRequest(); + request.setAccount("newuser"); + request.setPassword("password123"); + request.setUsername("New User"); + request.setEmail("newuser@example.com"); + request.setCaptcha("1234"); + request.setCaptchaKey("test-key"); + + AuthResponse response = new AuthResponse(); + response.setAccessToken("test-access-token"); + response.setRefreshToken("test-refresh-token"); + response.setExpiresIn(86400L); + + // Mock服务方法 + when(authService.register(any(RegisterRequest.class))).thenReturn(response); + + // 执行测试 + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("注册成功")); + } + + @Test + void testGenerateCaptcha() throws Exception { + // 准备测试数据 + CaptchaResponse response = new CaptchaResponse(); + response.setCaptchaKey("test-captcha-key"); + response.setCaptchaImage("data:image/png;base64,test-image"); + response.setExpiresIn(300L); + + // Mock服务方法 + when(authService.generateCaptcha()).thenReturn(response); + + // 执行测试 + mockMvc.perform(get("/auth/captcha")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.captchaKey").value("test-captcha-key")) + .andExpect(jsonPath("$.data.expiresIn").value(300)); + } + + @Test + void testValidateToken() throws Exception { + // Mock服务方法 + when(authService.validateToken("valid-token")).thenReturn(true); + + // 执行测试 + mockMvc.perform(get("/auth/validate") + .header("Authorization", "Bearer valid-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value(true)); + } + + @Test + void testLogout() throws Exception { + // Mock服务方法 + when(authService.logoutByToken("valid-token")).thenReturn(true); + + // 执行测试 + mockMvc.perform(post("/auth/logout") + .header("Authorization", "Bearer valid-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + } + + @Test + void testLoginWithInvalidCaptcha() throws Exception { + // 准备测试数据 + LoginRequest request = new LoginRequest(); + request.setAccount("testuser"); + request.setPassword("password123"); + request.setCaptcha("wrong"); + request.setCaptchaKey("test-key"); + + // Mock服务方法抛出异常 + when(authService.login(any(LoginRequest.class))).thenThrow(new CaptchaException("验证码错误")); + + // 执行测试 + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("验证码错误")); + } + + @Test + void testLoginWithInvalidAccount() throws Exception { + // 准备测试数据 + LoginRequest request = new LoginRequest(); + request.setAccount("nonexistent"); + request.setPassword("password123"); + request.setCaptcha("1234"); + request.setCaptchaKey("test-key"); + + // Mock服务方法抛出异常 + when(authService.login(any(LoginRequest.class))).thenThrow(new AuthException("账号不存在")); + + // 执行测试 + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(401)) + .andExpect(jsonPath("$.message").value("账号不存在")); + } +}