diff --git a/heritage-modules/heritage-portal/pom.xml b/heritage-modules/heritage-portal/pom.xml new file mode 100644 index 0000000..e5c6077 --- /dev/null +++ b/heritage-modules/heritage-portal/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-backend + ${revision} + ../../pom.xml + + + + heritage-portal + heritage-portal + 前台业务模块 + + + + + org.leocoder.heritage + heritage-common + ${revision} + + + + org.leocoder.heritage + heritage-resultex + ${revision} + + + + org.leocoder.heritage + heritage-sa-token + ${revision} + + + + org.leocoder.heritage + heritage-oper-logs + ${revision} + + + + org.leocoder.heritage + heritage-oss + ${revision} + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/auth/HrtAuthController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/auth/HrtAuthController.java new file mode 100644 index 0000000..2b561d5 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/auth/HrtAuthController.java @@ -0,0 +1,67 @@ +package org.leocoder.heritage.portal.controller.auth; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginResultVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtRegisterVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtUserInfoVo; +import org.leocoder.heritage.portal.service.auth.HrtAuthService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Leocoder + * @description [前台用户认证控制器] + */ +@Tag(name = "前台用户认证", description = "前台用户注册、登录、登出等认证功能") +@Validated +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@RestController +public class HrtAuthController { + + private final HrtAuthService hrtAuthService; + + /** + * @description [用户注册] + * @author Leocoder + */ + @Operation(summary = "用户注册", description = "前台用户注册接口") + @PostMapping("/register") + public String register(@Validated @RequestBody HrtRegisterVo vo) { + return hrtAuthService.register(vo); + } + + /** + * @description [用户登录] + * @author Leocoder + */ + @Operation(summary = "用户登录", description = "前台用户登录接口") + @PostMapping("/login") + public HrtLoginResultVo login(@Validated @RequestBody HrtLoginVo vo) { + return hrtAuthService.login(vo); + } + + /** + * @description [用户登出] + * @author Leocoder + */ + @Operation(summary = "用户登出", description = "前台用户登出接口") + @PostMapping("/logout") + public String logout() { + return hrtAuthService.logout(); + } + + /** + * @description [获取当前登录用户信息] + * @author Leocoder + */ + @Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息") + @GetMapping("/userInfo") + public HrtUserInfoVo getUserInfo() { + return hrtAuthService.getUserInfo(); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/comment/HrtCommentController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/comment/HrtCommentController.java new file mode 100644 index 0000000..59bff30 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/comment/HrtCommentController.java @@ -0,0 +1,68 @@ +package org.leocoder.heritage.portal.controller.comment; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.portal.service.comment.HrtCommentService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Leocoder + * @description [前台评论控制器] + */ +@Tag(name = "前台评论管理", description = "用户评论非遗项目、传承人、新闻等功能,支持回复和点赞") +@Validated +@RequestMapping("/api/comment") +@RequiredArgsConstructor +@RestController +public class HrtCommentController { + + private final HrtCommentService hrtCommentService; + + /** + * @description [发表评论] + * @author Leocoder + */ + @Operation(summary = "发表评论", description = "发表评论或回复,支持一级评论和二级回复") + @PostMapping("/add") + public String addComment(@Validated @RequestBody CommentAddVo vo) { + return hrtCommentService.addComment(vo); + } + + /** + * @description [删除自己的评论] + * @author Leocoder + */ + @Operation(summary = "删除评论", description = "删除自己发表的评论,同时会删除该评论下的所有回复") + @DeleteMapping("/delete/{commentId}") + public String deleteComment( + @Parameter(description = "评论ID") @PathVariable Long commentId + ) { + return hrtCommentService.deleteComment(commentId); + } + + /** + * @description [查看评论列表] + * @author Leocoder + */ + @Operation(summary = "查看评论列表", description = "根据目标类型和ID分页查询评论列表,包含回复和点赞状态") + @GetMapping("/list") + public IPage listComments(@Validated CommentQueryVo vo) { + return hrtCommentService.listComments(vo); + } + + /** + * @description [我的评论列表] + * @author Leocoder + */ + @Operation(summary = "我的评论列表", description = "分页查询当前用户的评论列表,支持按类型筛选") + @GetMapping("/myList") + public IPage myCommentList(@Validated MyCommentQueryVo vo) { + return hrtCommentService.myCommentList(vo); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/event/HrtEventController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/event/HrtEventController.java new file mode 100644 index 0000000..9702ade --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/event/HrtEventController.java @@ -0,0 +1,80 @@ +package org.leocoder.heritage.portal.controller.event; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.portal.service.event.HrtEventService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Leocoder + * @description [前台活动管理控制器] + */ +@Tag(name = "前台活动管理", description = "活动列表、详情、报名、取消报名等功能") +@Validated +@RequestMapping("/api/event") +@RequiredArgsConstructor +@RestController +public class HrtEventController { + + private final HrtEventService hrtEventService; + + /** + * @description [活动列表] + * @author Leocoder + */ + @Operation(summary = "活动列表", description = "分页查询活动列表,支持状态筛选和关键词搜索") + @GetMapping("/list") + public IPage listEvents(@Validated EventQueryVo vo) { + return hrtEventService.listEvents(vo); + } + + /** + * @description [活动详情] + * @author Leocoder + */ + @Operation(summary = "活动详情", description = "查看活动详情,自动增加浏览量,包含报名状态和是否可报名") + @GetMapping("/detail/{eventId}") + public EventDetailVo getEventDetail( + @Parameter(description = "活动ID") @PathVariable Long eventId + ) { + return hrtEventService.getEventDetail(eventId); + } + + /** + * @description [报名参加活动] + * @author Leocoder + */ + @Operation(summary = "报名活动", description = "报名参加活动,需验证报名时间和人数限制") + @PostMapping("/register") + public String registerEvent(@Validated @RequestBody EventRegistrationVo vo) { + return hrtEventService.registerEvent(vo); + } + + /** + * @description [取消报名] + * @author Leocoder + */ + @Operation(summary = "取消报名", description = "取消已报名的活动,活动开始后无法取消") + @PostMapping("/cancel/{eventId}") + public String cancelRegistration( + @Parameter(description = "活动ID") @PathVariable Long eventId + ) { + return hrtEventService.cancelRegistration(eventId); + } + + /** + * @description [我报名的活动] + * @author Leocoder + */ + @Operation(summary = "我的报名", description = "分页查询当前用户报名的活动列表,支持按状态筛选") + @GetMapping("/myRegistrations") + public IPage myRegistrations(@Validated MyEventRegistrationVo vo) { + return hrtEventService.myRegistrations(vo); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/favorite/HrtFavoriteController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/favorite/HrtFavoriteController.java new file mode 100644 index 0000000..e5c36c9 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/favorite/HrtFavoriteController.java @@ -0,0 +1,71 @@ +package org.leocoder.heritage.portal.controller.favorite; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteItemVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteOperateVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteQueryVo; +import org.leocoder.heritage.portal.service.favorite.HrtFavoriteService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Leocoder + * @description [前台收藏控制器] + */ +@Tag(name = "前台收藏管理", description = "用户收藏非遗项目、传承人、新闻等功能") +@Validated +@RequestMapping("/api/favorite") +@RequiredArgsConstructor +@RestController +public class HrtFavoriteController { + + private final HrtFavoriteService hrtFavoriteService; + + /** + * @description [添加收藏] + * @author Leocoder + */ + @Operation(summary = "添加收藏", description = "收藏非遗项目、传承人或新闻资讯") + @PostMapping("/add") + public String addFavorite(@Validated @RequestBody FavoriteOperateVo vo) { + return hrtFavoriteService.addFavorite(vo); + } + + /** + * @description [取消收藏] + * @author Leocoder + */ + @Operation(summary = "取消收藏", description = "取消已收藏的内容") + @PostMapping("/cancel") + public String cancelFavorite(@Validated @RequestBody FavoriteOperateVo vo) { + return hrtFavoriteService.cancelFavorite(vo); + } + + /** + * @description [我的收藏列表] + * @author Leocoder + */ + @Operation(summary = "我的收藏列表", description = "分页查询当前用户的收藏列表,支持按类型筛选") + @GetMapping("/myList") + public IPage myFavoriteList(@Validated FavoriteQueryVo vo) { + return hrtFavoriteService.myFavoriteList(vo); + } + + /** + * @description [检查是否已收藏] + * @author Leocoder + */ + @Operation(summary = "检查是否已收藏", description = "用于前端判断收藏按钮状态") + @GetMapping("/check") + public Boolean checkFavorite( + @Parameter(description = "目标类型:heritage、inheritor、news") @RequestParam String targetType, + @Parameter(description = "目标ID") @RequestParam Long targetId + ) { + return hrtFavoriteService.checkFavorite(targetType, targetId); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/heritage/HrtHeritageController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/heritage/HrtHeritageController.java new file mode 100644 index 0000000..fc6bb27 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/heritage/HrtHeritageController.java @@ -0,0 +1,70 @@ +package org.leocoder.heritage.portal.controller.heritage; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageQueryVo; +import org.leocoder.heritage.portal.service.heritage.HrtHeritageService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author Leocoder + * @description [前台非遗项目控制器] + */ +@Tag(name = "前台非遗项目管理", description = "非遗项目的查询、浏览、搜索等功能") +@Validated +@RequestMapping("/api/heritage") +@RequiredArgsConstructor +@RestController +public class HrtHeritageController { + + private final HrtHeritageService hrtHeritageService; + + /** + * @description [分页查询非遗项目列表] + * @author Leocoder + */ + @Operation(summary = "分页查询非遗项目列表", description = "支持关键词搜索(名称、描述)和多条件筛选(分类、标签、级别、地区等)") + @GetMapping("/list") + public IPage list(@Validated HrtHeritageQueryVo vo) { + return hrtHeritageService.listPage(vo); + } + + /** + * @description [查看非遗项目详情] + * @author Leocoder + */ + @Operation(summary = "查看非遗项目详情", description = "根据ID查看非遗项目的详细信息,浏览量自动+1") + @GetMapping("/detail/{id}") + public HrtHeritageDetailVo detail(@Parameter(description = "非遗项目ID") @PathVariable Long id) { + return hrtHeritageService.getDetail(id); + } + + /** + * @description [热门非遗项目] + * @author Leocoder + */ + @Operation(summary = "热门非遗项目", description = "根据浏览量和点赞数获取热门非遗项目列表") + @GetMapping("/hot") + public List hot(@Parameter(description = "查询数量,默认10条") @RequestParam(required = false, defaultValue = "10") Integer limit) { + return hrtHeritageService.getHotList(limit); + } + + /** + * @description [精选非遗项目] + * @author Leocoder + */ + @Operation(summary = "精选非遗项目", description = "获取平台精选的优质非遗项目") + @GetMapping("/featured") + public List featured(@Parameter(description = "查询数量,默认10条") @RequestParam(required = false, defaultValue = "10") Integer limit) { + return hrtHeritageService.getFeaturedList(limit); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/inheritor/HrtInheritorController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/inheritor/HrtInheritorController.java new file mode 100644 index 0000000..c7f1abd --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/inheritor/HrtInheritorController.java @@ -0,0 +1,70 @@ +package org.leocoder.heritage.portal.controller.inheritor; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorQueryVo; +import org.leocoder.heritage.portal.service.inheritor.HrtInheritorService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author Leocoder + * @description [前台传承人控制器] + */ +@Tag(name = "前台传承人管理", description = "传承人的查询、浏览、搜索等功能") +@Validated +@RequestMapping("/api/inheritor") +@RequiredArgsConstructor +@RestController +public class HrtInheritorController { + + private final HrtInheritorService hrtInheritorService; + + /** + * @description [分页查询传承人列表] + * @author Leocoder + */ + @Operation(summary = "分页查询传承人列表", description = "支持关键词搜索(姓名、简介、故事)和多条件筛选(级别、地区、非遗项目等)") + @GetMapping("/list") + public IPage list(@Validated HrtInheritorQueryVo vo) { + return hrtInheritorService.listPage(vo); + } + + /** + * @description [查看传承人详情] + * @author Leocoder + */ + @Operation(summary = "查看传承人详情", description = "根据ID查看传承人的详细信息,浏览量自动+1") + @GetMapping("/detail/{id}") + public HrtInheritorDetailVo detail(@Parameter(description = "传承人ID") @PathVariable Long id) { + return hrtInheritorService.getDetail(id); + } + + /** + * @description [根据非遗项目查询传承人] + * @author Leocoder + */ + @Operation(summary = "根据非遗项目查询传承人", description = "根据非遗项目ID查询该项目的所有传承人") + @GetMapping("/listByHeritage/{heritageId}") + public List listByHeritage(@Parameter(description = "非遗项目ID") @PathVariable Long heritageId) { + return hrtInheritorService.listByHeritageId(heritageId); + } + + /** + * @description [精选传承人] + * @author Leocoder + */ + @Operation(summary = "精选传承人", description = "获取平台精选的优秀传承人") + @GetMapping("/featured") + public List featured(@Parameter(description = "查询数量,默认10条") @RequestParam(required = false, defaultValue = "10") Integer limit) { + return hrtInheritorService.getFeaturedList(limit); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/like/HrtLikeController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/like/HrtLikeController.java new file mode 100644 index 0000000..2c34598 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/like/HrtLikeController.java @@ -0,0 +1,58 @@ +package org.leocoder.heritage.portal.controller.like; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.LikeOperateVo; +import org.leocoder.heritage.portal.service.like.HrtLikeService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Leocoder + * @description [前台点赞控制器] + */ +@Tag(name = "前台点赞管理", description = "用户点赞非遗项目、传承人、新闻、评论等功能") +@Validated +@RequestMapping("/api/like") +@RequiredArgsConstructor +@RestController +public class HrtLikeController { + + private final HrtLikeService hrtLikeService; + + /** + * @description [点赞] + * @author Leocoder + */ + @Operation(summary = "点赞", description = "点赞非遗项目、传承人、新闻或评论") + @PostMapping("/add") + public String addLike(@Validated @RequestBody LikeOperateVo vo) { + return hrtLikeService.addLike(vo); + } + + /** + * @description [取消点赞] + * @author Leocoder + */ + @Operation(summary = "取消点赞", description = "取消已点赞的内容") + @PostMapping("/cancel") + public String cancelLike(@Validated @RequestBody LikeOperateVo vo) { + return hrtLikeService.cancelLike(vo); + } + + /** + * @description [检查是否已点赞] + * @author Leocoder + */ + @Operation(summary = "检查是否已点赞", description = "用于前端判断点赞按钮状态") + @GetMapping("/check") + public Boolean checkLike( + @Parameter(description = "目标类型:heritage、inheritor、news、comment") @RequestParam String targetType, + @Parameter(description = "目标ID") @RequestParam Long targetId + ) { + return hrtLikeService.checkLike(targetType, targetId); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/news/HrtNewsController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/news/HrtNewsController.java new file mode 100644 index 0000000..aed2edf --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/news/HrtNewsController.java @@ -0,0 +1,72 @@ +package org.leocoder.heritage.portal.controller.news; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.portal.service.news.HrtNewsService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author Leocoder + * @description [前台新闻资讯控制器] + */ +@Tag(name = "前台新闻资讯", description = "新闻资讯列表、详情、热门、置顶等功能") +@Validated +@RequestMapping("/api/news") +@RequiredArgsConstructor +@RestController +public class HrtNewsController { + + private final HrtNewsService hrtNewsService; + + /** + * @description [分页查询新闻列表] + * @author Leocoder + */ + @Operation(summary = "新闻列表", description = "分页查询新闻列表,支持分类筛选和关键词搜索") + @GetMapping("/list") + public IPage listNews(@Validated NewsQueryVo vo) { + return hrtNewsService.listNews(vo); + } + + /** + * @description [查看新闻详情] + * @author Leocoder + */ + @Operation(summary = "新闻详情", description = "查看新闻详情,自动增加浏览量,包含点赞和收藏状态") + @GetMapping("/detail/{newsId}") + public NewsDetailVo getNewsDetail( + @Parameter(description = "新闻ID") @PathVariable Long newsId + ) { + return hrtNewsService.getNewsDetail(newsId); + } + + /** + * @description [热门新闻] + * @author Leocoder + */ + @Operation(summary = "热门新闻", description = "根据浏览量获取热门新闻列表") + @GetMapping("/hot") + public List hotNews( + @Parameter(description = "返回数量,默认10") @RequestParam(required = false, defaultValue = "10") Integer limit + ) { + return hrtNewsService.hotNews(limit); + } + + /** + * @description [置顶新闻] + * @author Leocoder + */ + @Operation(summary = "置顶新闻", description = "获取所有置顶新闻列表") + @GetMapping("/top") + public List topNews() { + return hrtNewsService.topNews(); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/user/HrtUserController.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/user/HrtUserController.java new file mode 100644 index 0000000..ad34861 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/controller/user/HrtUserController.java @@ -0,0 +1,96 @@ +package org.leocoder.heritage.portal.controller.user; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.portal.service.user.HrtUserService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author Leocoder + * @description [前台用户中心控制器] + */ +@Tag(name = "前台用户中心", description = "个人资料管理、密码修改、用户统计、浏览历史等功能") +@Validated +@RequestMapping("/api/user") +@RequiredArgsConstructor +@RestController +public class HrtUserController { + + private final HrtUserService hrtUserService; + + /** + * @description [获取个人资料] + * @author Leocoder + */ + @Operation(summary = "获取个人资料", description = "获取当前登录用户的个人资料信息") + @GetMapping("/profile") + public UserProfileVo getProfile() { + return hrtUserService.getProfile(); + } + + /** + * @description [修改个人资料] + * @author Leocoder + */ + @Operation(summary = "修改个人资料", description = "修改昵称、邮箱、手机号、性别、生日等信息") + @PutMapping("/profile") + public String updateProfile(@Validated @RequestBody UserProfileUpdateVo vo) { + return hrtUserService.updateProfile(vo); + } + + /** + * @description [更新头像] + * @author Leocoder + */ + @Operation(summary = "更新头像", description = "更新用户头像URL") + @PutMapping("/avatar") + public String updateAvatar(@Validated @RequestBody UserAvatarUpdateVo vo) { + return hrtUserService.updateAvatar(vo); + } + + /** + * @description [上传并更新头像] + * @author Leocoder + */ + @Operation(summary = "上传头像", description = "上传头像文件并自动更新用户头像,支持jpg/png/gif等格式,限制2MB以内") + @PostMapping("/avatar/upload") + public String uploadAvatar(@RequestParam("file") MultipartFile file) { + return hrtUserService.uploadAndUpdateAvatar(file); + } + + /** + * @description [修改密码] + * @author Leocoder + */ + @Operation(summary = "修改密码", description = "修改登录密码,需验证旧密码") + @PutMapping("/password") + public String updatePassword(@Validated @RequestBody UserPasswordUpdateVo vo) { + return hrtUserService.updatePassword(vo); + } + + /** + * @description [获取用户统计信息] + * @author Leocoder + */ + @Operation(summary = "用户统计", description = "获取用户的浏览、评论、点赞、收藏、报名等统计数据") + @GetMapping("/stats") + public UserStatsVo getUserStats() { + return hrtUserService.getUserStats(); + } + + /** + * @description [获取浏览历史] + * @author Leocoder + */ + @Operation(summary = "浏览历史", description = "分页查询用户的浏览历史记录,支持按类型筛选") + @GetMapping("/viewHistory") + public IPage getViewHistory(@Validated ViewHistoryQueryVo vo) { + return hrtUserService.getViewHistory(vo); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthService.java new file mode 100644 index 0000000..1f59c9d --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthService.java @@ -0,0 +1,38 @@ +package org.leocoder.heritage.portal.service.auth; + +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginResultVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtRegisterVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtUserInfoVo; + +/** + * @author Leocoder + * @description [前台用户认证服务接口] + */ +public interface HrtAuthService { + + /** + * @description [用户注册] + * @author Leocoder + */ + String register(HrtRegisterVo vo); + + /** + * @description [用户登录] + * @author Leocoder + */ + HrtLoginResultVo login(HrtLoginVo vo); + + /** + * @description [用户登出] + * @author Leocoder + */ + String logout(); + + /** + * @description [获取当前登录用户信息] + * @author Leocoder + */ + HrtUserInfoVo getUserInfo(); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthServiceImpl.java new file mode 100644 index 0000000..ccbf705 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/auth/HrtAuthServiceImpl.java @@ -0,0 +1,166 @@ +package org.leocoder.heritage.portal.service.auth; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.leocoder.heritage.common.exception.coder.YUtil; +import org.leocoder.heritage.common.utils.ip.IpUtil; +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginResultVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtLoginVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtRegisterVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtUserInfoVo; +import org.leocoder.heritage.domain.pojo.portal.HrtUser; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtUserMapper; +import org.leocoder.heritage.satoken.config.CoderSaTokenPasswordUtil; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.time.LocalDateTime; + +/** + * @author Leocoder + * @description [前台用户认证服务实现类] + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class HrtAuthServiceImpl implements HrtAuthService { + + private final HrtUserMapper hrtUserMapper; + + /** + * @description [用户注册] + * @author Leocoder + */ + @Override + public String register(HrtRegisterVo vo) { + // 1、校验两次密码是否一致 + YUtil.isTrue(!vo.getPassword().equals(vo.getConfirmPassword()), "两次密码输入不一致"); + + // 2、校验用户名是否已存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtUser::getUsername, vo.getUsername()); + HrtUser existUser = hrtUserMapper.selectOne(queryWrapper); + YUtil.isTrue(existUser != null, "用户名已存在"); + + // 3、校验邮箱是否已存在(如果提供了邮箱) + if (StringUtils.isNotBlank(vo.getEmail())) { + queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtUser::getEmail, vo.getEmail()); + existUser = hrtUserMapper.selectOne(queryWrapper); + YUtil.isTrue(existUser != null, "邮箱已被注册"); + } + + // 4、校验手机号是否已存在(如果提供了手机号) + if (StringUtils.isNotBlank(vo.getPhone())) { + queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtUser::getPhone, vo.getPhone()); + existUser = hrtUserMapper.selectOne(queryWrapper); + YUtil.isTrue(existUser != null, "手机号已被注册"); + } + + // 5、创建新用户 + HrtUser newUser = new HrtUser(); + newUser.setUsername(vo.getUsername()); + newUser.setPassword(CoderSaTokenPasswordUtil.encryptPassword(vo.getPassword())); + newUser.setNickname(vo.getNickname()); + newUser.setEmail(vo.getEmail()); + newUser.setPhone(vo.getPhone()); + newUser.setStatus(1); + newUser.setGender(0); + + // 6、保存用户 + int result = hrtUserMapper.insert(newUser); + YUtil.isTrue(result <= 0, "注册失败"); + + log.info("前台用户注册成功:{}", vo.getUsername()); + return "注册成功"; + } + + /** + * @description [用户登录] + * @author Leocoder + */ + @Override + public HrtLoginResultVo login(HrtLoginVo vo) { + // 1、查询用户(支持用户名、邮箱、手机号登录) + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.and(wrapper -> wrapper + .eq(HrtUser::getUsername, vo.getAccount()) + .or() + .eq(HrtUser::getEmail, vo.getAccount()) + .or() + .eq(HrtUser::getPhone, vo.getAccount()) + ); + HrtUser user = hrtUserMapper.selectOne(queryWrapper); + YUtil.isTrue(user == null, "账号或密码错误"); + + // 2、校验用户状态 + YUtil.isTrue(user.getStatus() == 0, "账号已被禁用,请联系管理员"); + + // 3、校验密码 + boolean passwordMatch = CoderSaTokenPasswordUtil.matchesPassword(vo.getPassword(), user.getPassword()); + YUtil.isTrue(!passwordMatch, "账号或密码错误"); + + // 4、执行登录 + HrtStpUtil.login(user.getId()); + + // 5、设置记住我 + if (vo.getRememberMe() != null && vo.getRememberMe()) { + HrtStpUtil.getStpLogic().getTokenSession().updateTimeout(60 * 60 * 24 * 7); + } + + // 6、更新登录信息 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attributes != null ? attributes.getRequest() : null; + String loginIp = request != null ? IpUtil.getIpAddr(request) : "unknown"; + + HrtUser updateUser = new HrtUser(); + updateUser.setId(user.getId()); + updateUser.setLoginIp(loginIp); + updateUser.setLoginTime(LocalDateTime.now()); + hrtUserMapper.updateById(updateUser); + + // 7、构建返回数据 + HrtLoginResultVo result = new HrtLoginResultVo(); + result.setToken(HrtStpUtil.getTokenValue()); + result.setTokenName(HrtStpUtil.getTokenName()); + result.setTokenTimeout(HrtStpUtil.getStpLogic().getTokenTimeout()); + + HrtUserInfoVo userInfo = BeanUtil.copyProperties(user, HrtUserInfoVo.class); + result.setUserInfo(userInfo); + + log.info("前台用户登录成功:{}", user.getUsername()); + return result; + } + + /** + * @description [用户登出] + * @author Leocoder + */ + @Override + public String logout() { + HrtStpUtil.logout(); + log.info("前台用户登出成功"); + return "登出成功"; + } + + /** + * @description [获取当前登录用户信息] + * @author Leocoder + */ + @Override + public HrtUserInfoVo getUserInfo() { + Long userId = HrtStpUtil.getLoginIdAsLong(); + HrtUser user = hrtUserMapper.selectById(userId); + YUtil.isTrue(user == null, "用户不存在"); + + return BeanUtil.copyProperties(user, HrtUserInfoVo.class); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentService.java new file mode 100644 index 0000000..c6fc9dc --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentService.java @@ -0,0 +1,36 @@ +package org.leocoder.heritage.portal.service.comment; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.*; + +/** + * @author Leocoder + * @description [评论服务接口] + */ +public interface HrtCommentService { + + /** + * @description [发表评论] + * @author Leocoder + */ + String addComment(CommentAddVo vo); + + /** + * @description [删除自己的评论] + * @author Leocoder + */ + String deleteComment(Long commentId); + + /** + * @description [查看评论列表(按目标ID和类型)] + * @author Leocoder + */ + IPage listComments(CommentQueryVo vo); + + /** + * @description [我的评论列表] + * @author Leocoder + */ + IPage myCommentList(MyCommentQueryVo vo); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentServiceImpl.java new file mode 100644 index 0000000..7a67ad3 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/comment/HrtCommentServiceImpl.java @@ -0,0 +1,343 @@ +package org.leocoder.heritage.portal.service.comment; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.enums.TargetTypeEnum; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.domain.pojo.portal.*; +import org.leocoder.heritage.mybatisplus.mapper.portal.*; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [评论服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtCommentServiceImpl implements HrtCommentService { + + private final HrtCommentMapper hrtCommentMapper; + private final HrtUserMapper hrtUserMapper; + private final HrtHeritageMapper hrtHeritageMapper; + private final HrtInheritorMapper hrtInheritorMapper; + private final HrtNewsMapper hrtNewsMapper; + private final HrtLikeMapper hrtLikeMapper; + + /** + * @description [发表评论] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String addComment(CommentAddVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + if (!TargetTypeEnum.isValid(vo.getTargetType())) { + throw new BusinessException(400, "不支持的目标类型"); + } + + validateTargetExists(vo.getTargetType(), vo.getTargetId()); + + if (vo.getParentId() != null && vo.getParentId() > 0) { + HrtComment parentComment = hrtCommentMapper.selectById(vo.getParentId()); + if (parentComment == null) { + throw new BusinessException(404, "父评论不存在"); + } + if (!parentComment.getTargetType().equals(vo.getTargetType()) + || !parentComment.getTargetId().equals(vo.getTargetId())) { + throw new BusinessException(400, "父评论与目标不匹配"); + } + } + + if (vo.getRating() != null && !"heritage".equals(vo.getTargetType())) { + throw new BusinessException(400, "只有非遗项目支持评分"); + } + + HrtComment comment = new HrtComment(); + comment.setUserId(userId); + comment.setTargetType(vo.getTargetType()); + comment.setTargetId(vo.getTargetId()); + comment.setContent(vo.getContent()); + comment.setRating(vo.getRating()); + comment.setParentId(vo.getParentId() == null ? 0L : vo.getParentId()); + comment.setLikeCount(0); + comment.setStatus(1); + + int result = hrtCommentMapper.insert(comment); + if (result <= 0) { + throw new BusinessException(500, "发表评论失败"); + } + + return "评论成功"; + } + + /** + * @description [删除自己的评论] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String deleteComment(Long commentId) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtComment comment = hrtCommentMapper.selectById(commentId); + if (comment == null) { + throw new BusinessException(404, "评论不存在"); + } + + if (!comment.getUserId().equals(userId)) { + throw new BusinessException(403, "无权删除他人评论"); + } + + int result = hrtCommentMapper.deleteById(commentId); + if (result <= 0) { + throw new BusinessException(500, "删除评论失败"); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtComment::getParentId, commentId); + hrtCommentMapper.delete(wrapper); + + return "删除成功"; + } + + /** + * @description [查看评论列表(按目标ID和类型)] + * @author Leocoder + */ + @Override + public IPage listComments(CommentQueryVo vo) { + Long currentUserId = null; + try { + currentUserId = HrtStpUtil.getLoginIdAsLong(); + } catch (Exception e) { + } + + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtComment::getTargetType, vo.getTargetType()) + .eq(HrtComment::getTargetId, vo.getTargetId()) + .eq(HrtComment::getParentId, 0L) + .eq(HrtComment::getStatus, 1); + + if ("hottest".equals(vo.getSortType())) { + wrapper.orderByDesc(HrtComment::getLikeCount); + } else { + wrapper.orderByDesc(HrtComment::getCreateTime); + } + + IPage commentPage = hrtCommentMapper.selectPage(page, wrapper); + + List userIds = commentPage.getRecords().stream() + .map(HrtComment::getUserId) + .distinct() + .collect(Collectors.toList()); + + Map userMap = userIds.isEmpty() ? Map.of() : + hrtUserMapper.selectBatchIds(userIds).stream() + .collect(Collectors.toMap(HrtUser::getId, user -> user)); + + List commentIds = commentPage.getRecords().stream() + .map(HrtComment::getId) + .collect(Collectors.toList()); + + Map> replyMap = Map.of(); + if (!commentIds.isEmpty()) { + LambdaQueryWrapper replyWrapper = new LambdaQueryWrapper<>(); + replyWrapper.in(HrtComment::getParentId, commentIds) + .eq(HrtComment::getStatus, 1) + .orderByAsc(HrtComment::getCreateTime); + List replies = hrtCommentMapper.selectList(replyWrapper); + + List replyUserIds = replies.stream() + .map(HrtComment::getUserId) + .distinct() + .filter(uid -> !userMap.containsKey(uid)) + .collect(Collectors.toList()); + + if (!replyUserIds.isEmpty()) { + hrtUserMapper.selectBatchIds(replyUserIds).forEach(user -> + userMap.put(user.getId(), user) + ); + } + + replyMap = replies.stream() + .collect(Collectors.groupingBy(HrtComment::getParentId)); + } + + Map likeMap = Map.of(); + if (currentUserId != null && !commentIds.isEmpty()) { + List allCommentIds = new ArrayList<>(commentIds); + replyMap.values().forEach(replyList -> + replyList.forEach(reply -> allCommentIds.add(reply.getId())) + ); + + LambdaQueryWrapper likeWrapper = new LambdaQueryWrapper<>(); + likeWrapper.eq(HrtLike::getUserId, currentUserId) + .eq(HrtLike::getTargetType, "comment") + .in(HrtLike::getTargetId, allCommentIds); + likeMap = hrtLikeMapper.selectList(likeWrapper).stream() + .collect(Collectors.toMap(HrtLike::getTargetId, like -> true)); + } + + final Map finalLikeMap = likeMap; + final Map> finalReplyMap = replyMap; + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(commentPage.getTotal()); + resultPage.setRecords(commentPage.getRecords().stream() + .map(comment -> buildCommentItemVo(comment, userMap, finalReplyMap, finalLikeMap)) + .collect(Collectors.toList()) + ); + + return resultPage; + } + + /** + * @description [我的评论列表] + * @author Leocoder + */ + @Override + public IPage myCommentList(MyCommentQueryVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtComment::getUserId, userId); + + if (vo.getTargetType() != null && !vo.getTargetType().isEmpty()) { + wrapper.eq(HrtComment::getTargetType, vo.getTargetType()); + } + + wrapper.orderByDesc(HrtComment::getCreateTime); + + IPage commentPage = hrtCommentMapper.selectPage(page, wrapper); + + HrtUser currentUser = hrtUserMapper.selectById(userId); + + Map likeMap = Map.of(); + if (!commentPage.getRecords().isEmpty()) { + List commentIds = commentPage.getRecords().stream() + .map(HrtComment::getId) + .collect(Collectors.toList()); + + LambdaQueryWrapper likeWrapper = new LambdaQueryWrapper<>(); + likeWrapper.eq(HrtLike::getUserId, userId) + .eq(HrtLike::getTargetType, "comment") + .in(HrtLike::getTargetId, commentIds); + likeMap = hrtLikeMapper.selectList(likeWrapper).stream() + .collect(Collectors.toMap(HrtLike::getTargetId, like -> true)); + } + + final Map finalLikeMap = likeMap; + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(commentPage.getTotal()); + resultPage.setRecords(commentPage.getRecords().stream() + .map(comment -> { + CommentItemVo itemVo = BeanUtil.copyProperties(comment, CommentItemVo.class); + itemVo.setUserName(currentUser.getNickname()); + itemVo.setUserAvatar(currentUser.getAvatar()); + itemVo.setIsLiked(finalLikeMap.getOrDefault(comment.getId(), false)); + itemVo.setReplyCount(0); + return itemVo; + }) + .collect(Collectors.toList()) + ); + + return resultPage; + } + + /** + * @description [验证目标对象是否存在] + * @author Leocoder + */ + private void validateTargetExists(String targetType, Long targetId) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + throw new BusinessException(400, "无效的目标类型"); + } + + switch (typeEnum) { + case HERITAGE: + HrtHeritage heritage = hrtHeritageMapper.selectById(targetId); + if (heritage == null || heritage.getPublishStatus() != 1) { + throw new BusinessException(404, "非遗项目不存在或未发布"); + } + break; + case INHERITOR: + HrtInheritor inheritor = hrtInheritorMapper.selectById(targetId); + if (inheritor == null || inheritor.getPublishStatus() != 1) { + throw new BusinessException(404, "传承人不存在或未发布"); + } + break; + case NEWS: + HrtNews news = hrtNewsMapper.selectById(targetId); + if (news == null || news.getPublishStatus() != 1) { + throw new BusinessException(404, "新闻不存在或未发布"); + } + break; + case COMMENT: + throw new BusinessException(400, "不支持对评论进行评论"); + default: + throw new BusinessException(400, "不支持的目标类型"); + } + } + + /** + * @description [构建评论项VO] + * @author Leocoder + */ + private CommentItemVo buildCommentItemVo(HrtComment comment, + Map userMap, + Map> replyMap, + Map likeMap) { + CommentItemVo itemVo = BeanUtil.copyProperties(comment, CommentItemVo.class); + + HrtUser user = userMap.get(comment.getUserId()); + if (user != null) { + itemVo.setUserName(user.getNickname()); + itemVo.setUserAvatar(user.getAvatar()); + } + + itemVo.setIsLiked(likeMap.getOrDefault(comment.getId(), false)); + + List replies = replyMap.getOrDefault(comment.getId(), List.of()); + itemVo.setReplyCount(replies.size()); + + if (!replies.isEmpty()) { + List replyVos = replies.stream() + .map(reply -> { + CommentItemVo replyVo = BeanUtil.copyProperties(reply, CommentItemVo.class); + HrtUser replyUser = userMap.get(reply.getUserId()); + if (replyUser != null) { + replyVo.setUserName(replyUser.getNickname()); + replyVo.setUserAvatar(replyUser.getAvatar()); + } + replyVo.setIsLiked(likeMap.getOrDefault(reply.getId(), false)); + replyVo.setReplyCount(0); + return replyVo; + }) + .collect(Collectors.toList()); + itemVo.setReplies(replyVos); + } else { + itemVo.setReplies(new ArrayList<>()); + } + + return itemVo; + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventService.java new file mode 100644 index 0000000..9fd39ef --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventService.java @@ -0,0 +1,42 @@ +package org.leocoder.heritage.portal.service.event; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.*; + +/** + * @author Leocoder + * @description [活动管理服务接口] + */ +public interface HrtEventService { + + /** + * @description [活动列表] + * @author Leocoder + */ + IPage listEvents(EventQueryVo vo); + + /** + * @description [活动详情] + * @author Leocoder + */ + EventDetailVo getEventDetail(Long eventId); + + /** + * @description [报名参加活动] + * @author Leocoder + */ + String registerEvent(EventRegistrationVo vo); + + /** + * @description [取消报名] + * @author Leocoder + */ + String cancelRegistration(Long eventId); + + /** + * @description [我报名的活动] + * @author Leocoder + */ + IPage myRegistrations(MyEventRegistrationVo vo); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventServiceImpl.java new file mode 100644 index 0000000..6393e48 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/event/HrtEventServiceImpl.java @@ -0,0 +1,320 @@ +package org.leocoder.heritage.portal.service.event; + +import cn.hutool.core.bean.BeanUtil; +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 lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.domain.pojo.portal.HrtEvent; +import org.leocoder.heritage.domain.pojo.portal.HrtEventRegistration; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtEventMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtEventRegistrationMapper; +import org.leocoder.heritage.portal.service.user.HrtUserService; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [活动管理服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtEventServiceImpl implements HrtEventService { + + private final HrtEventMapper hrtEventMapper; + private final HrtEventRegistrationMapper hrtEventRegistrationMapper; + private final HrtUserService hrtUserService; + + /** + * @description [活动列表] + * @author Leocoder + */ + @Override + public IPage listEvents(EventQueryVo vo) { + Long currentUserId = null; + try { + currentUserId = HrtStpUtil.getLoginIdAsLong(); + } catch (Exception e) { + } + + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtEvent::getPublishStatus, 1); + + if (vo.getStatus() != null && !vo.getStatus().isEmpty()) { + wrapper.eq(HrtEvent::getStatus, vo.getStatus()); + } + + if (vo.getKeyword() != null && !vo.getKeyword().isEmpty()) { + wrapper.and(w -> w.like(HrtEvent::getTitle, vo.getKeyword()) + .or() + .like(HrtEvent::getSummary, vo.getKeyword()) + .or() + .like(HrtEvent::getContent, vo.getKeyword())); + } + + wrapper.orderByDesc(HrtEvent::getStartTime); + + IPage eventPage = hrtEventMapper.selectPage(page, wrapper); + + Map registrationMap = Map.of(); + if (currentUserId != null && !eventPage.getRecords().isEmpty()) { + List eventIds = eventPage.getRecords().stream() + .map(HrtEvent::getId) + .collect(Collectors.toList()); + + LambdaQueryWrapper regWrapper = new LambdaQueryWrapper<>(); + regWrapper.eq(HrtEventRegistration::getUserId, currentUserId) + .in(HrtEventRegistration::getEventId, eventIds) + .in(HrtEventRegistration::getStatus, List.of("pending", "approved")); + + registrationMap = hrtEventRegistrationMapper.selectList(regWrapper).stream() + .collect(Collectors.toMap(HrtEventRegistration::getEventId, reg -> true)); + } + + final Map finalRegistrationMap = registrationMap; + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(eventPage.getTotal()); + resultPage.setRecords(eventPage.getRecords().stream() + .map(event -> { + EventListVo listVo = BeanUtil.copyProperties(event, EventListVo.class); + listVo.setIsRegistered(finalRegistrationMap.getOrDefault(event.getId(), false)); + return listVo; + }) + .collect(Collectors.toList()) + ); + + return resultPage; + } + + /** + * @description [活动详情] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public EventDetailVo getEventDetail(Long eventId) { + HrtEvent event = hrtEventMapper.selectById(eventId); + if (event == null || event.getPublishStatus() != 1) { + throw new BusinessException(404, "活动不存在或未发布"); + } + + hrtEventMapper.update(null, + new LambdaUpdateWrapper() + .setSql("view_count = view_count + 1") + .eq(HrtEvent::getId, eventId) + ); + + EventDetailVo detailVo = BeanUtil.copyProperties(event, EventDetailVo.class); + detailVo.setViewCount(event.getViewCount() + 1); + + hrtUserService.recordViewHistory("event", eventId); + + Long currentUserId = null; + try { + currentUserId = HrtStpUtil.getLoginIdAsLong(); + } catch (Exception e) { + } + + if (currentUserId != null) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtEventRegistration::getUserId, currentUserId) + .eq(HrtEventRegistration::getEventId, eventId) + .in(HrtEventRegistration::getStatus, List.of("pending", "approved")); + Long count = hrtEventRegistrationMapper.selectCount(wrapper); + detailVo.setIsRegistered(count > 0); + } + + LocalDateTime now = LocalDateTime.now(); + boolean canRegister = event.getStatus().equals("upcoming") + && event.getRegistrationStart() != null + && event.getRegistrationEnd() != null + && now.isAfter(event.getRegistrationStart()) + && now.isBefore(event.getRegistrationEnd()) + && (event.getMaxParticipants() == null || event.getCurrentParticipants() < event.getMaxParticipants()) + && !detailVo.getIsRegistered(); + detailVo.setCanRegister(canRegister); + + return detailVo; + } + + /** + * @description [报名参加活动] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String registerEvent(EventRegistrationVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtEvent event = hrtEventMapper.selectById(vo.getEventId()); + if (event == null || event.getPublishStatus() != 1) { + throw new BusinessException(404, "活动不存在或未发布"); + } + + if (!event.getStatus().equals("upcoming")) { + throw new BusinessException(400, "该活动不在报名阶段"); + } + + LocalDateTime now = LocalDateTime.now(); + if (event.getRegistrationStart() == null || event.getRegistrationEnd() == null) { + throw new BusinessException(400, "活动报名时间未设置"); + } + + if (now.isBefore(event.getRegistrationStart())) { + throw new BusinessException(400, "报名尚未开始"); + } + + if (now.isAfter(event.getRegistrationEnd())) { + throw new BusinessException(400, "报名已结束"); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtEventRegistration::getUserId, userId) + .eq(HrtEventRegistration::getEventId, vo.getEventId()) + .in(HrtEventRegistration::getStatus, List.of("pending", "approved")); + Long count = hrtEventRegistrationMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(400, "您已报名该活动"); + } + + if (event.getMaxParticipants() != null && event.getCurrentParticipants() >= event.getMaxParticipants()) { + throw new BusinessException(400, "活动报名人数已满"); + } + + HrtEventRegistration registration = new HrtEventRegistration(); + registration.setUserId(userId); + registration.setEventId(vo.getEventId()); + registration.setStatus("approved"); + registration.setPhone(vo.getPhone()); + registration.setRemark(vo.getRemark()); + + int result = hrtEventRegistrationMapper.insert(registration); + if (result <= 0) { + throw new BusinessException(500, "报名失败"); + } + + hrtEventMapper.update(null, + new LambdaUpdateWrapper() + .setSql("current_participants = current_participants + 1") + .eq(HrtEvent::getId, vo.getEventId()) + ); + + return "报名成功"; + } + + /** + * @description [取消报名] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String cancelRegistration(Long eventId) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtEventRegistration::getUserId, userId) + .eq(HrtEventRegistration::getEventId, eventId) + .in(HrtEventRegistration::getStatus, List.of("pending", "approved")); + HrtEventRegistration registration = hrtEventRegistrationMapper.selectOne(wrapper); + + if (registration == null) { + throw new BusinessException(400, "您未报名该活动"); + } + + HrtEvent event = hrtEventMapper.selectById(eventId); + if (event == null) { + throw new BusinessException(404, "活动不存在"); + } + + LocalDateTime now = LocalDateTime.now(); + if (event.getStartTime() != null && now.isAfter(event.getStartTime())) { + throw new BusinessException(400, "活动已开始,无法取消报名"); + } + + registration.setStatus("cancelled"); + int result = hrtEventRegistrationMapper.updateById(registration); + if (result <= 0) { + throw new BusinessException(500, "取消报名失败"); + } + + hrtEventMapper.update(null, + new LambdaUpdateWrapper() + .setSql("current_participants = GREATEST(current_participants - 1, 0)") + .eq(HrtEvent::getId, eventId) + ); + + return "取消报名成功"; + } + + /** + * @description [我报名的活动] + * @author Leocoder + */ + @Override + public IPage myRegistrations(MyEventRegistrationVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtEventRegistration::getUserId, userId); + + if (vo.getStatus() != null && !vo.getStatus().isEmpty()) { + wrapper.eq(HrtEventRegistration::getStatus, vo.getStatus()); + } + + wrapper.orderByDesc(HrtEventRegistration::getCreateTime); + + IPage registrationPage = hrtEventRegistrationMapper.selectPage(page, wrapper); + + List eventIds = registrationPage.getRecords().stream() + .map(HrtEventRegistration::getEventId) + .collect(Collectors.toList()); + + Map eventMap = eventIds.isEmpty() ? Map.of() : + hrtEventMapper.selectBatchIds(eventIds).stream() + .collect(Collectors.toMap(HrtEvent::getId, event -> event)); + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(registrationPage.getTotal()); + resultPage.setRecords(registrationPage.getRecords().stream() + .map(registration -> { + MyEventRegistrationItemVo itemVo = new MyEventRegistrationItemVo(); + itemVo.setId(registration.getId()); + itemVo.setEventId(registration.getEventId()); + itemVo.setRegistrationStatus(registration.getStatus()); + itemVo.setPhone(registration.getPhone()); + itemVo.setRemark(registration.getRemark()); + itemVo.setCreateTime(registration.getCreateTime()); + + HrtEvent event = eventMap.get(registration.getEventId()); + if (event != null) { + itemVo.setEventTitle(event.getTitle()); + itemVo.setEventCoverImage(event.getCoverImage()); + itemVo.setEventLocation(event.getLocation()); + itemVo.setEventStartTime(event.getStartTime()); + itemVo.setEventEndTime(event.getEndTime()); + itemVo.setEventStatus(event.getStatus()); + } + + return itemVo; + }) + .collect(Collectors.toList()) + ); + + return resultPage; + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteService.java new file mode 100644 index 0000000..7ccf05d --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteService.java @@ -0,0 +1,38 @@ +package org.leocoder.heritage.portal.service.favorite; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteItemVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteOperateVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteQueryVo; + +/** + * @author Leocoder + * @description [收藏服务接口] + */ +public interface HrtFavoriteService { + + /** + * @description [添加收藏] + * @author Leocoder + */ + String addFavorite(FavoriteOperateVo vo); + + /** + * @description [取消收藏] + * @author Leocoder + */ + String cancelFavorite(FavoriteOperateVo vo); + + /** + * @description [我的收藏列表] + * @author Leocoder + */ + IPage myFavoriteList(FavoriteQueryVo vo); + + /** + * @description [检查是否已收藏] + * @author Leocoder + */ + Boolean checkFavorite(String targetType, Long targetId); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteServiceImpl.java new file mode 100644 index 0000000..2c2a082 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/favorite/HrtFavoriteServiceImpl.java @@ -0,0 +1,306 @@ +package org.leocoder.heritage.portal.service.favorite; + +import cn.hutool.core.util.StrUtil; +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 lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.enums.TargetTypeEnum; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteItemVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteOperateVo; +import org.leocoder.heritage.domain.model.vo.portal.FavoriteQueryVo; +import org.leocoder.heritage.domain.pojo.portal.HrtFavorite; +import org.leocoder.heritage.domain.pojo.portal.HrtHeritage; +import org.leocoder.heritage.domain.pojo.portal.HrtInheritor; +import org.leocoder.heritage.domain.pojo.portal.HrtNews; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtFavoriteMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtHeritageMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtInheritorMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtNewsMapper; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Leocoder + * @description [收藏服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtFavoriteServiceImpl implements HrtFavoriteService { + + private final HrtFavoriteMapper hrtFavoriteMapper; + private final HrtHeritageMapper hrtHeritageMapper; + private final HrtInheritorMapper hrtInheritorMapper; + private final HrtNewsMapper hrtNewsMapper; + + /** + * @description [添加收藏] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String addFavorite(FavoriteOperateVo vo) { + // 1、验证目标类型是否支持收藏 + if (!TargetTypeEnum.isFavoriteSupported(vo.getTargetType())) { + throw new BusinessException(400, "不支持的收藏类型"); + } + + // 2、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 3、检查是否已收藏(查询有效记录) + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtFavorite::getUserId, userId) + .eq(HrtFavorite::getTargetType, vo.getTargetType()) + .eq(HrtFavorite::getTargetId, vo.getTargetId()); + HrtFavorite existingFavorite = hrtFavoriteMapper.selectOne(queryWrapper); + if (existingFavorite != null) { + throw new BusinessException(400, "您已经收藏过了"); + } + + // 4、检查是否存在逻辑删除的记录(使用自定义方法绕过@TableLogic过滤) + HrtFavorite deletedFavorite = hrtFavoriteMapper.selectDeletedOne(userId, vo.getTargetType(), vo.getTargetId()); + + if (deletedFavorite != null) { + // 5、恢复逻辑删除的记录(使用原生SQL绕过@TableLogic限制) + int result = hrtFavoriteMapper.restoreDeleted(deletedFavorite.getId()); + if (result <= 0) { + throw new BusinessException(500, "收藏失败"); + } + } else { + // 6、验证目标对象是否存在且已发布 + validateTargetExists(vo.getTargetType(), vo.getTargetId()); + + // 7、插入新的收藏记录 + HrtFavorite favorite = new HrtFavorite(); + favorite.setUserId(userId); + favorite.setTargetType(vo.getTargetType()); + favorite.setTargetId(vo.getTargetId()); + int result = hrtFavoriteMapper.insert(favorite); + if (result <= 0) { + throw new BusinessException(500, "收藏失败"); + } + } + + // 8、更新目标对象的收藏计数 +1 + updateFavoriteCount(vo.getTargetType(), vo.getTargetId(), 1); + + return "收藏成功"; + } + + /** + * @description [取消收藏] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String cancelFavorite(FavoriteOperateVo vo) { + // 1、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 2、查找收藏记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtFavorite::getUserId, userId) + .eq(HrtFavorite::getTargetType, vo.getTargetType()) + .eq(HrtFavorite::getTargetId, vo.getTargetId()); + HrtFavorite favorite = hrtFavoriteMapper.selectOne(queryWrapper); + if (favorite == null) { + throw new BusinessException(400, "您还未收藏该内容"); + } + + // 3、逻辑删除收藏记录 + int result = hrtFavoriteMapper.deleteById(favorite.getId()); + if (result <= 0) { + throw new BusinessException(500, "取消收藏失败"); + } + + // 4、更新目标对象的收藏计数 -1 + updateFavoriteCount(vo.getTargetType(), vo.getTargetId(), -1); + + return "取消收藏成功"; + } + + /** + * @description [我的收藏列表] + * @author Leocoder + */ + @Override + public IPage myFavoriteList(FavoriteQueryVo vo) { + // 1、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 2、分页查询收藏记录 + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtFavorite::getUserId, userId); + + // 按类型筛选(可选) + if (StrUtil.isNotBlank(vo.getTargetType())) { + if (!TargetTypeEnum.isFavoriteSupported(vo.getTargetType())) { + throw new BusinessException(400, "不支持的收藏类型"); + } + queryWrapper.eq(HrtFavorite::getTargetType, vo.getTargetType()); + } + + // 按创建时间倒序 + queryWrapper.orderByDesc(HrtFavorite::getCreateTime); + + IPage favoritePageResult = hrtFavoriteMapper.selectPage(page, queryWrapper); + + // 3、转换为VO并填充目标对象信息 + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(favoritePageResult.getTotal()); + resultPage.setPages(favoritePageResult.getPages()); + + List itemList = new ArrayList<>(); + for (HrtFavorite favorite : favoritePageResult.getRecords()) { + FavoriteItemVo itemVo = new FavoriteItemVo(); + itemVo.setId(favorite.getId()); + itemVo.setTargetType(favorite.getTargetType()); + itemVo.setTargetId(favorite.getTargetId()); + itemVo.setCreateTime(favorite.getCreateTime()); + + // 填充目标对象信息 + fillTargetInfo(itemVo, favorite.getTargetType(), favorite.getTargetId()); + itemList.add(itemVo); + } + + resultPage.setRecords(itemList); + return resultPage; + } + + /** + * @description [检查是否已收藏] + * @author Leocoder + */ + @Override + public Boolean checkFavorite(String targetType, Long targetId) { + // 1、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 2、查询收藏记录是否存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtFavorite::getUserId, userId) + .eq(HrtFavorite::getTargetType, targetType) + .eq(HrtFavorite::getTargetId, targetId); + Long count = hrtFavoriteMapper.selectCount(queryWrapper); + + return count > 0; + } + + /** + * @description [验证目标对象是否存在且已发布] + * @author Leocoder + */ + private void validateTargetExists(String targetType, Long targetId) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + throw new BusinessException(400, "无效的目标类型"); + } + + switch (typeEnum) { + case HERITAGE: + HrtHeritage heritage = hrtHeritageMapper.selectById(targetId); + if (heritage == null || heritage.getPublishStatus() != 1) { + throw new BusinessException(404, "非遗项目不存在或未发布"); + } + break; + case INHERITOR: + HrtInheritor inheritor = hrtInheritorMapper.selectById(targetId); + if (inheritor == null || inheritor.getPublishStatus() != 1) { + throw new BusinessException(404, "传承人不存在或未发布"); + } + break; + case NEWS: + HrtNews news = hrtNewsMapper.selectById(targetId); + if (news == null || news.getPublishStatus() != 1) { + throw new BusinessException(404, "新闻不存在或未发布"); + } + break; + default: + throw new BusinessException(400, "不支持的收藏类型"); + } + } + + /** + * @description [更新收藏计数(原子操作)] + * @author Leocoder + */ + private void updateFavoriteCount(String targetType, Long targetId, int delta) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + return; + } + + String sqlSet = delta > 0 + ? "favorite_count = favorite_count + " + delta + : "favorite_count = GREATEST(favorite_count + (" + delta + "), 0)"; + + switch (typeEnum) { + case HERITAGE: + hrtHeritageMapper.update(null, + new LambdaUpdateWrapper() + .setSql(sqlSet) + .eq(HrtHeritage::getId, targetId) + ); + break; + case INHERITOR: + // 传承人表没有收藏计数字段,跳过 + break; + case NEWS: + // 新闻表没有收藏计数字段,跳过 + break; + } + } + + /** + * @description [填充目标对象信息] + * @author Leocoder + */ + private void fillTargetInfo(FavoriteItemVo itemVo, String targetType, Long targetId) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + return; + } + + switch (typeEnum) { + case HERITAGE: + HrtHeritage heritage = hrtHeritageMapper.selectById(targetId); + if (heritage != null) { + itemVo.setTargetTitle(heritage.getName()); + itemVo.setTargetCover(heritage.getCoverImage()); + itemVo.setTargetDescription(heritage.getDescription()); + itemVo.setTargetViewCount(heritage.getViewCount()); + itemVo.setTargetLikeCount(heritage.getLikeCount()); + } + break; + case INHERITOR: + HrtInheritor inheritor = hrtInheritorMapper.selectById(targetId); + if (inheritor != null) { + itemVo.setTargetTitle(inheritor.getName()); + itemVo.setTargetCover(inheritor.getAvatar()); + itemVo.setTargetDescription(inheritor.getIntroduction()); + itemVo.setTargetViewCount(inheritor.getViewCount()); + itemVo.setTargetLikeCount(inheritor.getLikeCount()); + } + break; + case NEWS: + HrtNews news = hrtNewsMapper.selectById(targetId); + if (news != null) { + itemVo.setTargetTitle(news.getTitle()); + itemVo.setTargetCover(news.getCoverImage()); + itemVo.setTargetDescription(news.getSummary()); + itemVo.setTargetViewCount(news.getViewCount()); + itemVo.setTargetLikeCount(news.getLikeCount()); + } + break; + } + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageService.java new file mode 100644 index 0000000..e190b8d --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageService.java @@ -0,0 +1,40 @@ +package org.leocoder.heritage.portal.service.heritage; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageQueryVo; + +import java.util.List; + +/** + * @author Leocoder + * @description [非遗项目服务接口] + */ +public interface HrtHeritageService { + + /** + * @description [分页查询非遗项目列表] + * @author Leocoder + */ + IPage listPage(HrtHeritageQueryVo vo); + + /** + * @description [查看非遗项目详情] + * @author Leocoder + */ + HrtHeritageDetailVo getDetail(Long id); + + /** + * @description [热门非遗项目] + * @author Leocoder + */ + List getHotList(Integer limit); + + /** + * @description [精选非遗项目] + * @author Leocoder + */ + List getFeaturedList(Integer limit); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageServiceImpl.java new file mode 100644 index 0000000..0cced04 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/heritage/HrtHeritageServiceImpl.java @@ -0,0 +1,194 @@ +package org.leocoder.heritage.portal.service.heritage; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtHeritageQueryVo; +import org.leocoder.heritage.domain.pojo.portal.HrtHeritage; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtHeritageMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [非遗项目服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtHeritageServiceImpl implements HrtHeritageService { + + private final HrtHeritageMapper hrtHeritageMapper; + private final org.leocoder.heritage.portal.service.user.HrtUserService hrtUserService; + + /** + * @description [分页查询非遗项目列表] + * @author Leocoder + */ + @Override + public IPage listPage(HrtHeritageQueryVo vo) { + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = buildQueryWrapper(vo); + + // 只查询已发布的项目 + wrapper.eq(HrtHeritage::getPublishStatus, 1); + + // 排序 + applySorting(wrapper, vo.getSortField(), vo.getSortOrder()); + + IPage heritagePageResult = hrtHeritageMapper.selectPage(page, wrapper); + + // 转换为VO + return heritagePageResult.convert(heritage -> BeanUtil.copyProperties(heritage, HrtHeritageListVo.class)); + } + + /** + * @description [查看非遗项目详情] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public HrtHeritageDetailVo getDetail(Long id) { + HrtHeritage heritage = hrtHeritageMapper.selectById(id); + + if (heritage == null || heritage.getPublishStatus() != 1) { + throw new BusinessException(404, "非遗项目不存在或未发布"); + } + + // 浏览量+1 + HrtHeritage updateHeritage = new HrtHeritage(); + updateHeritage.setId(id); + updateHeritage.setViewCount(heritage.getViewCount() + 1); + hrtHeritageMapper.updateById(updateHeritage); + + // 记录浏览历史 + hrtUserService.recordViewHistory("heritage", id); + + // 转换为详情VO + HrtHeritageDetailVo detailVo = BeanUtil.copyProperties(heritage, HrtHeritageDetailVo.class); + detailVo.setViewCount(heritage.getViewCount() + 1); + + return detailVo; + } + + /** + * @description [热门非遗项目] + * @author Leocoder + */ + @Override + public List getHotList(Integer limit) { + // 默认查询10条 + if (limit == null || limit <= 0) { + limit = 10; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtHeritage::getPublishStatus, 1) + .orderByDesc(HrtHeritage::getViewCount, HrtHeritage::getLikeCount) + .last("LIMIT " + limit); + + List heritageList = hrtHeritageMapper.selectList(wrapper); + + return heritageList.stream() + .map(heritage -> BeanUtil.copyProperties(heritage, HrtHeritageListVo.class)) + .collect(Collectors.toList()); + } + + /** + * @description [精选非遗项目] + * @author Leocoder + */ + @Override + public List getFeaturedList(Integer limit) { + // 默认查询10条 + if (limit == null || limit <= 0) { + limit = 10; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtHeritage::getPublishStatus, 1) + .eq(HrtHeritage::getIsFeatured, 1) + .orderByDesc(HrtHeritage::getSortOrder, HrtHeritage::getCreateTime) + .last("LIMIT " + limit); + + List heritageList = hrtHeritageMapper.selectList(wrapper); + + return heritageList.stream() + .map(heritage -> BeanUtil.copyProperties(heritage, HrtHeritageListVo.class)) + .collect(Collectors.toList()); + } + + /** + * @description [构建查询条件] + * @author Leocoder + */ + private LambdaQueryWrapper buildQueryWrapper(HrtHeritageQueryVo vo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索(搜索名称、英文名称、描述) + if (StrUtil.isNotBlank(vo.getKeyword())) { + wrapper.and(w -> w + .like(HrtHeritage::getName, vo.getKeyword()) + .or().like(HrtHeritage::getNameEn, vo.getKeyword()) + .or().like(HrtHeritage::getDescription, vo.getKeyword()) + ); + } + + // 名称模糊查询(兼容旧版本) + wrapper.like(StrUtil.isNotBlank(vo.getName()), HrtHeritage::getName, vo.getName()); + + // 分类 + wrapper.eq(StrUtil.isNotBlank(vo.getCategory()), HrtHeritage::getCategory, vo.getCategory()); + + // 级别 + wrapper.eq(StrUtil.isNotBlank(vo.getLevel()), HrtHeritage::getLevel, vo.getLevel()); + + // 省份 + wrapper.eq(StrUtil.isNotBlank(vo.getProvince()), HrtHeritage::getProvince, vo.getProvince()); + + // 城市 + wrapper.eq(StrUtil.isNotBlank(vo.getCity()), HrtHeritage::getCity, vo.getCity()); + + // 状态 + wrapper.eq(StrUtil.isNotBlank(vo.getStatus()), HrtHeritage::getStatus, vo.getStatus()); + + // 标签模糊匹配 + wrapper.like(StrUtil.isNotBlank(vo.getTag()), HrtHeritage::getTags, vo.getTag()); + + return wrapper; + } + + /** + * @description [应用排序规则] + * @author Leocoder + */ + private void applySorting(LambdaQueryWrapper wrapper, String sortField, String sortOrder) { + boolean isAsc = "asc".equalsIgnoreCase(sortOrder); + + switch (sortField) { + case "view_count": + wrapper.orderBy(true, isAsc, HrtHeritage::getViewCount); + break; + case "like_count": + wrapper.orderBy(true, isAsc, HrtHeritage::getLikeCount); + break; + case "favorite_count": + wrapper.orderBy(true, isAsc, HrtHeritage::getFavoriteCount); + break; + case "create_time": + default: + wrapper.orderBy(true, isAsc, HrtHeritage::getCreateTime); + break; + } + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorService.java new file mode 100644 index 0000000..439ac08 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorService.java @@ -0,0 +1,40 @@ +package org.leocoder.heritage.portal.service.inheritor; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorQueryVo; + +import java.util.List; + +/** + * @author Leocoder + * @description [传承人服务接口] + */ +public interface HrtInheritorService { + + /** + * @description [分页查询传承人列表] + * @author Leocoder + */ + IPage listPage(HrtInheritorQueryVo vo); + + /** + * @description [查看传承人详情] + * @author Leocoder + */ + HrtInheritorDetailVo getDetail(Long id); + + /** + * @description [根据非遗项目查询传承人] + * @author Leocoder + */ + List listByHeritageId(Long heritageId); + + /** + * @description [精选传承人] + * @author Leocoder + */ + List getFeaturedList(Integer limit); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorServiceImpl.java new file mode 100644 index 0000000..a5fce24 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/inheritor/HrtInheritorServiceImpl.java @@ -0,0 +1,185 @@ +package org.leocoder.heritage.portal.service.inheritor; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorListVo; +import org.leocoder.heritage.domain.model.vo.portal.HrtInheritorQueryVo; +import org.leocoder.heritage.domain.pojo.portal.HrtInheritor; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtInheritorMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [传承人服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtInheritorServiceImpl implements HrtInheritorService { + + private final HrtInheritorMapper hrtInheritorMapper; + private final org.leocoder.heritage.portal.service.user.HrtUserService hrtUserService; + + /** + * @description [分页查询传承人列表] + * @author Leocoder + */ + @Override + public IPage listPage(HrtInheritorQueryVo vo) { + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = buildQueryWrapper(vo); + + // 只查询已发布的传承人 + wrapper.eq(HrtInheritor::getPublishStatus, 1); + + // 排序 + applySorting(wrapper, vo.getSortField(), vo.getSortOrder()); + + IPage inheritorPageResult = hrtInheritorMapper.selectPage(page, wrapper); + + // 转换为VO + return inheritorPageResult.convert(inheritor -> BeanUtil.copyProperties(inheritor, HrtInheritorListVo.class)); + } + + /** + * @description [查看传承人详情] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public HrtInheritorDetailVo getDetail(Long id) { + HrtInheritor inheritor = hrtInheritorMapper.selectById(id); + + if (inheritor == null || inheritor.getPublishStatus() != 1) { + throw new BusinessException(404, "传承人不存在或未发布"); + } + + // 浏览量+1 + HrtInheritor updateInheritor = new HrtInheritor(); + updateInheritor.setId(id); + updateInheritor.setViewCount(inheritor.getViewCount() + 1); + hrtInheritorMapper.updateById(updateInheritor); + + // 记录浏览历史 + hrtUserService.recordViewHistory("inheritor", id); + + // 转换为详情VO + HrtInheritorDetailVo detailVo = BeanUtil.copyProperties(inheritor, HrtInheritorDetailVo.class); + detailVo.setViewCount(inheritor.getViewCount() + 1); + + return detailVo; + } + + /** + * @description [根据非遗项目查询传承人] + * @author Leocoder + */ + @Override + public List listByHeritageId(Long heritageId) { + if (heritageId == null) { + throw new BusinessException(400, "非遗项目ID不能为空"); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtInheritor::getHeritageId, heritageId) + .eq(HrtInheritor::getPublishStatus, 1) + .orderByDesc(HrtInheritor::getSortOrder, HrtInheritor::getCreateTime); + + List inheritorList = hrtInheritorMapper.selectList(wrapper); + + return inheritorList.stream() + .map(inheritor -> BeanUtil.copyProperties(inheritor, HrtInheritorListVo.class)) + .collect(Collectors.toList()); + } + + /** + * @description [精选传承人] + * @author Leocoder + */ + @Override + public List getFeaturedList(Integer limit) { + // 默认查询10条 + if (limit == null || limit <= 0) { + limit = 10; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtInheritor::getPublishStatus, 1) + .eq(HrtInheritor::getIsFeatured, 1) + .orderByDesc(HrtInheritor::getSortOrder, HrtInheritor::getCreateTime) + .last("LIMIT " + limit); + + List inheritorList = hrtInheritorMapper.selectList(wrapper); + + return inheritorList.stream() + .map(inheritor -> BeanUtil.copyProperties(inheritor, HrtInheritorListVo.class)) + .collect(Collectors.toList()); + } + + /** + * @description [构建查询条件] + * @author Leocoder + */ + private LambdaQueryWrapper buildQueryWrapper(HrtInheritorQueryVo vo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索(搜索姓名、英文名、简介、传承故事) + if (StrUtil.isNotBlank(vo.getKeyword())) { + wrapper.and(w -> w + .like(HrtInheritor::getName, vo.getKeyword()) + .or().like(HrtInheritor::getNameEn, vo.getKeyword()) + .or().like(HrtInheritor::getIntroduction, vo.getKeyword()) + .or().like(HrtInheritor::getStory, vo.getKeyword()) + ); + } + + // 姓名模糊查询(兼容旧版本) + wrapper.like(StrUtil.isNotBlank(vo.getName()), HrtInheritor::getName, vo.getName()); + + // 传承人级别 + wrapper.eq(StrUtil.isNotBlank(vo.getLevel()), HrtInheritor::getLevel, vo.getLevel()); + + // 省份 + wrapper.eq(StrUtil.isNotBlank(vo.getProvince()), HrtInheritor::getProvince, vo.getProvince()); + + // 城市 + wrapper.eq(StrUtil.isNotBlank(vo.getCity()), HrtInheritor::getCity, vo.getCity()); + + // 关联非遗项目ID + wrapper.eq(vo.getHeritageId() != null, HrtInheritor::getHeritageId, vo.getHeritageId()); + + return wrapper; + } + + /** + * @description [应用排序规则] + * @author Leocoder + */ + private void applySorting(LambdaQueryWrapper wrapper, String sortField, String sortOrder) { + boolean isAsc = "asc".equalsIgnoreCase(sortOrder); + + switch (sortField) { + case "view_count": + wrapper.orderBy(true, isAsc, HrtInheritor::getViewCount); + break; + case "like_count": + wrapper.orderBy(true, isAsc, HrtInheritor::getLikeCount); + break; + case "create_time": + default: + wrapper.orderBy(true, isAsc, HrtInheritor::getCreateTime); + break; + } + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeService.java new file mode 100644 index 0000000..be7bd80 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeService.java @@ -0,0 +1,29 @@ +package org.leocoder.heritage.portal.service.like; + +import org.leocoder.heritage.domain.model.vo.portal.LikeOperateVo; + +/** + * @author Leocoder + * @description [点赞服务接口] + */ +public interface HrtLikeService { + + /** + * @description [点赞] + * @author Leocoder + */ + String addLike(LikeOperateVo vo); + + /** + * @description [取消点赞] + * @author Leocoder + */ + String cancelLike(LikeOperateVo vo); + + /** + * @description [检查是否已点赞] + * @author Leocoder + */ + Boolean checkLike(String targetType, Long targetId); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeServiceImpl.java new file mode 100644 index 0000000..b4ccb63 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/like/HrtLikeServiceImpl.java @@ -0,0 +1,221 @@ +package org.leocoder.heritage.portal.service.like; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.enums.TargetTypeEnum; +import org.leocoder.heritage.domain.model.vo.portal.LikeOperateVo; +import org.leocoder.heritage.domain.pojo.portal.*; +import org.leocoder.heritage.mybatisplus.mapper.portal.*; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Leocoder + * @description [点赞服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtLikeServiceImpl implements HrtLikeService { + + private final HrtLikeMapper hrtLikeMapper; + private final HrtHeritageMapper hrtHeritageMapper; + private final HrtInheritorMapper hrtInheritorMapper; + private final HrtNewsMapper hrtNewsMapper; + private final HrtCommentMapper hrtCommentMapper; + + /** + * @description [点赞] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String addLike(LikeOperateVo vo) { + // 1、验证目标类型是否支持点赞 + if (!TargetTypeEnum.isLikeSupported(vo.getTargetType())) { + throw new BusinessException(400, "不支持的点赞类型"); + } + + // 2、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 3、检查是否已点赞(查询有效记录) + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtLike::getUserId, userId) + .eq(HrtLike::getTargetType, vo.getTargetType()) + .eq(HrtLike::getTargetId, vo.getTargetId()); + HrtLike existingLike = hrtLikeMapper.selectOne(queryWrapper); + if (existingLike != null) { + throw new BusinessException(400, "您已经点赞过了"); + } + + // 4、检查是否存在逻辑删除的记录(使用自定义方法绕过@TableLogic过滤) + HrtLike deletedLike = hrtLikeMapper.selectDeletedOne(userId, vo.getTargetType(), vo.getTargetId()); + + if (deletedLike != null) { + // 5、恢复逻辑删除的记录(使用原生SQL绕过@TableLogic限制) + int result = hrtLikeMapper.restoreDeleted(deletedLike.getId()); + if (result <= 0) { + throw new BusinessException(500, "点赞失败"); + } + } else { + // 6、验证目标对象是否存在 + validateTargetExists(vo.getTargetType(), vo.getTargetId()); + + // 7、插入新的点赞记录 + HrtLike like = new HrtLike(); + like.setUserId(userId); + like.setTargetType(vo.getTargetType()); + like.setTargetId(vo.getTargetId()); + int result = hrtLikeMapper.insert(like); + if (result <= 0) { + throw new BusinessException(500, "点赞失败"); + } + } + + // 8、更新目标对象的点赞计数 +1 + updateLikeCount(vo.getTargetType(), vo.getTargetId(), 1); + + return "点赞成功"; + } + + /** + * @description [取消点赞] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String cancelLike(LikeOperateVo vo) { + // 1、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 2、查找点赞记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtLike::getUserId, userId) + .eq(HrtLike::getTargetType, vo.getTargetType()) + .eq(HrtLike::getTargetId, vo.getTargetId()); + HrtLike like = hrtLikeMapper.selectOne(queryWrapper); + if (like == null) { + throw new BusinessException(400, "您还未点赞该内容"); + } + + // 3、逻辑删除点赞记录 + int result = hrtLikeMapper.deleteById(like.getId()); + if (result <= 0) { + throw new BusinessException(500, "取消点赞失败"); + } + + // 4、更新目标对象的点赞计数 -1 + updateLikeCount(vo.getTargetType(), vo.getTargetId(), -1); + + return "取消点赞成功"; + } + + /** + * @description [检查是否已点赞] + * @author Leocoder + */ + @Override + public Boolean checkLike(String targetType, Long targetId) { + // 1、获取当前登录用户ID + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 2、查询点赞记录是否存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(HrtLike::getUserId, userId) + .eq(HrtLike::getTargetType, targetType) + .eq(HrtLike::getTargetId, targetId); + Long count = hrtLikeMapper.selectCount(queryWrapper); + + return count > 0; + } + + /** + * @description [验证目标对象是否存在] + * @author Leocoder + */ + private void validateTargetExists(String targetType, Long targetId) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + throw new BusinessException(400, "无效的目标类型"); + } + + switch (typeEnum) { + case HERITAGE: + HrtHeritage heritage = hrtHeritageMapper.selectById(targetId); + if (heritage == null || heritage.getPublishStatus() != 1) { + throw new BusinessException(404, "非遗项目不存在或未发布"); + } + break; + case INHERITOR: + HrtInheritor inheritor = hrtInheritorMapper.selectById(targetId); + if (inheritor == null || inheritor.getPublishStatus() != 1) { + throw new BusinessException(404, "传承人不存在或未发布"); + } + break; + case NEWS: + HrtNews news = hrtNewsMapper.selectById(targetId); + if (news == null || news.getPublishStatus() != 1) { + throw new BusinessException(404, "新闻不存在或未发布"); + } + break; + case COMMENT: + HrtComment comment = hrtCommentMapper.selectById(targetId); + if (comment == null || comment.getStatus() != 1) { + throw new BusinessException(404, "评论不存在或未通过审核"); + } + break; + default: + throw new BusinessException(400, "不支持的点赞类型"); + } + } + + /** + * @description [更新点赞计数(原子操作)] + * @author Leocoder + */ + private void updateLikeCount(String targetType, Long targetId, int delta) { + TargetTypeEnum typeEnum = TargetTypeEnum.fromCode(targetType); + if (typeEnum == null) { + return; + } + + String sqlSet = delta > 0 + ? "like_count = like_count + " + delta + : "like_count = GREATEST(like_count + (" + delta + "), 0)"; + + switch (typeEnum) { + case HERITAGE: + hrtHeritageMapper.update(null, + new LambdaUpdateWrapper() + .setSql(sqlSet) + .eq(HrtHeritage::getId, targetId) + ); + break; + case INHERITOR: + hrtInheritorMapper.update(null, + new LambdaUpdateWrapper() + .setSql(sqlSet) + .eq(HrtInheritor::getId, targetId) + ); + break; + case NEWS: + hrtNewsMapper.update(null, + new LambdaUpdateWrapper() + .setSql(sqlSet) + .eq(HrtNews::getId, targetId) + ); + break; + case COMMENT: + hrtCommentMapper.update(null, + new LambdaUpdateWrapper() + .setSql(sqlSet) + .eq(HrtComment::getId, targetId) + ); + break; + } + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsService.java new file mode 100644 index 0000000..7fcec66 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsService.java @@ -0,0 +1,38 @@ +package org.leocoder.heritage.portal.service.news; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.*; + +import java.util.List; + +/** + * @author Leocoder + * @description [新闻资讯服务接口] + */ +public interface HrtNewsService { + + /** + * @description [分页查询新闻列表] + * @author Leocoder + */ + IPage listNews(NewsQueryVo vo); + + /** + * @description [查看新闻详情] + * @author Leocoder + */ + NewsDetailVo getNewsDetail(Long newsId); + + /** + * @description [热门新闻] + * @author Leocoder + */ + List hotNews(Integer limit); + + /** + * @description [置顶新闻] + * @author Leocoder + */ + List topNews(); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsServiceImpl.java new file mode 100644 index 0000000..3f7fb8b --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/news/HrtNewsServiceImpl.java @@ -0,0 +1,166 @@ +package org.leocoder.heritage.portal.service.news; + +import cn.hutool.core.bean.BeanUtil; +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 lombok.RequiredArgsConstructor; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.model.vo.portal.NewsDetailVo; +import org.leocoder.heritage.domain.model.vo.portal.NewsListVo; +import org.leocoder.heritage.domain.model.vo.portal.NewsQueryVo; +import org.leocoder.heritage.domain.pojo.portal.HrtFavorite; +import org.leocoder.heritage.domain.pojo.portal.HrtLike; +import org.leocoder.heritage.domain.pojo.portal.HrtNews; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtFavoriteMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtLikeMapper; +import org.leocoder.heritage.mybatisplus.mapper.portal.HrtNewsMapper; +import org.leocoder.heritage.portal.service.user.HrtUserService; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [新闻资讯服务实现类] + */ +@Service +@RequiredArgsConstructor +public class HrtNewsServiceImpl implements HrtNewsService { + + private final HrtNewsMapper hrtNewsMapper; + private final HrtLikeMapper hrtLikeMapper; + private final HrtFavoriteMapper hrtFavoriteMapper; + private final HrtUserService hrtUserService; + + /** + * @description [分页查询新闻列表] + * @author Leocoder + */ + @Override + public IPage listNews(NewsQueryVo vo) { + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtNews::getPublishStatus, 1); + + if (vo.getCategory() != null && !vo.getCategory().isEmpty()) { + wrapper.eq(HrtNews::getCategory, vo.getCategory()); + } + + if (vo.getKeyword() != null && !vo.getKeyword().isEmpty()) { + wrapper.and(w -> w.like(HrtNews::getTitle, vo.getKeyword()) + .or() + .like(HrtNews::getSummary, vo.getKeyword()) + .or() + .like(HrtNews::getContent, vo.getKeyword())); + } + + wrapper.orderByDesc(HrtNews::getIsTop) + .orderByDesc(HrtNews::getPublishTime); + + IPage newsPage = hrtNewsMapper.selectPage(page, wrapper); + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(newsPage.getTotal()); + resultPage.setRecords(newsPage.getRecords().stream() + .map(news -> BeanUtil.copyProperties(news, NewsListVo.class)) + .collect(Collectors.toList()) + ); + + return resultPage; + } + + /** + * @description [查看新闻详情] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public NewsDetailVo getNewsDetail(Long newsId) { + HrtNews news = hrtNewsMapper.selectById(newsId); + if (news == null || news.getPublishStatus() != 1) { + throw new BusinessException(404, "新闻不存在或未发布"); + } + + hrtNewsMapper.update(null, + new LambdaUpdateWrapper() + .setSql("view_count = view_count + 1") + .eq(HrtNews::getId, newsId) + ); + + NewsDetailVo detailVo = BeanUtil.copyProperties(news, NewsDetailVo.class); + detailVo.setViewCount(news.getViewCount() + 1); + + hrtUserService.recordViewHistory("news", newsId); + + Long currentUserId = null; + try { + currentUserId = HrtStpUtil.getLoginIdAsLong(); + } catch (Exception e) { + } + + if (currentUserId != null) { + LambdaQueryWrapper likeWrapper = new LambdaQueryWrapper<>(); + likeWrapper.eq(HrtLike::getUserId, currentUserId) + .eq(HrtLike::getTargetType, "news") + .eq(HrtLike::getTargetId, newsId); + Long likeCount = hrtLikeMapper.selectCount(likeWrapper); + detailVo.setIsLiked(likeCount > 0); + + LambdaQueryWrapper favoriteWrapper = new LambdaQueryWrapper<>(); + favoriteWrapper.eq(HrtFavorite::getUserId, currentUserId) + .eq(HrtFavorite::getTargetType, "news") + .eq(HrtFavorite::getTargetId, newsId); + Long favoriteCount = hrtFavoriteMapper.selectCount(favoriteWrapper); + detailVo.setIsFavorited(favoriteCount > 0); + } + + return detailVo; + } + + /** + * @description [热门新闻] + * @author Leocoder + */ + @Override + public List hotNews(Integer limit) { + if (limit == null || limit <= 0) { + limit = 10; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtNews::getPublishStatus, 1) + .orderByDesc(HrtNews::getViewCount) + .last("LIMIT " + limit); + + List newsList = hrtNewsMapper.selectList(wrapper); + + return newsList.stream() + .map(news -> BeanUtil.copyProperties(news, NewsListVo.class)) + .collect(Collectors.toList()); + } + + /** + * @description [置顶新闻] + * @author Leocoder + */ + @Override + public List topNews() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtNews::getPublishStatus, 1) + .eq(HrtNews::getIsTop, 1) + .orderByDesc(HrtNews::getPublishTime); + + List newsList = hrtNewsMapper.selectList(wrapper); + + return newsList.stream() + .map(news -> BeanUtil.copyProperties(news, NewsListVo.class)) + .collect(Collectors.toList()); + } + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserService.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserService.java new file mode 100644 index 0000000..7650e48 --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserService.java @@ -0,0 +1,61 @@ +package org.leocoder.heritage.portal.service.user; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author Leocoder + * @description [用户中心服务接口] + */ +public interface HrtUserService { + + /** + * @description [获取个人资料] + * @author Leocoder + */ + UserProfileVo getProfile(); + + /** + * @description [修改个人资料] + * @author Leocoder + */ + String updateProfile(UserProfileUpdateVo vo); + + /** + * @description [更新头像] + * @author Leocoder + */ + String updateAvatar(UserAvatarUpdateVo vo); + + /** + * @description [上传并更新头像] + * @author Leocoder + */ + String uploadAndUpdateAvatar(MultipartFile file); + + /** + * @description [修改密码] + * @author Leocoder + */ + String updatePassword(UserPasswordUpdateVo vo); + + /** + * @description [获取用户统计信息] + * @author Leocoder + */ + UserStatsVo getUserStats(); + + /** + * @description [获取浏览历史] + * @author Leocoder + */ + IPage getViewHistory(ViewHistoryQueryVo vo); + + /** + * @description [记录浏览历史] + * @author Leocoder + */ + void recordViewHistory(String targetType, Long targetId); + +} diff --git a/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserServiceImpl.java b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserServiceImpl.java new file mode 100644 index 0000000..f0f202b --- /dev/null +++ b/heritage-modules/heritage-portal/src/main/java/org/leocoder/heritage/portal/service/user/HrtUserServiceImpl.java @@ -0,0 +1,478 @@ +package org.leocoder.heritage.portal.service.user; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.leocoder.heritage.common.exception.BusinessException; +import org.leocoder.heritage.domain.enums.TargetTypeEnum; +import org.leocoder.heritage.domain.model.vo.portal.*; +import org.leocoder.heritage.domain.pojo.portal.*; +import org.leocoder.heritage.mybatisplus.mapper.portal.*; +import org.leocoder.heritage.oss.service.StorageService; +import org.leocoder.heritage.oss.service.StorageServiceFactory; +import org.leocoder.heritage.oss.utils.OssUtil; +import org.leocoder.heritage.satoken.config.CoderSaTokenPasswordUtil; +import org.leocoder.heritage.satoken.util.HrtStpUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Leocoder + * @description [用户中心服务实现类] + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class HrtUserServiceImpl implements HrtUserService { + + private final HrtUserMapper hrtUserMapper; + private final HrtViewHistoryMapper hrtViewHistoryMapper; + private final HrtCommentMapper hrtCommentMapper; + private final HrtLikeMapper hrtLikeMapper; + private final HrtFavoriteMapper hrtFavoriteMapper; + private final HrtEventRegistrationMapper hrtEventRegistrationMapper; + private final HrtHeritageMapper hrtHeritageMapper; + private final HrtInheritorMapper hrtInheritorMapper; + private final HrtNewsMapper hrtNewsMapper; + private final HrtEventMapper hrtEventMapper; + private final StorageServiceFactory storageServiceFactory; + + @Value("${coder.storage.type:local}") + private String storageType; + + // 允许的头像文件扩展名 + private static final List ALLOWED_AVATAR_EXTENSIONS = Arrays.asList( + "jpg", "jpeg", "png", "gif", "webp" + ); + + // 头像文件大小限制:2MB + private static final long MAX_AVATAR_SIZE = 2 * 1024 * 1024; + + /** + * @description [获取个人资料] + * @author Leocoder + */ + @Override + public UserProfileVo getProfile() { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtUser user = hrtUserMapper.selectById(userId); + if (user == null) { + throw new BusinessException(404, "用户不存在"); + } + + return BeanUtil.copyProperties(user, UserProfileVo.class); + } + + /** + * @description [修改个人资料] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String updateProfile(UserProfileUpdateVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtUser user = hrtUserMapper.selectById(userId); + if (user == null) { + throw new BusinessException(404, "用户不存在"); + } + + if (vo.getEmail() != null && !vo.getEmail().isEmpty()) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtUser::getEmail, vo.getEmail()) + .ne(HrtUser::getId, userId); + Long count = hrtUserMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(400, "该邮箱已被使用"); + } + } + + if (vo.getPhone() != null && !vo.getPhone().isEmpty()) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtUser::getPhone, vo.getPhone()) + .ne(HrtUser::getId, userId); + Long count = hrtUserMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(400, "该手机号已被使用"); + } + } + + BeanUtil.copyProperties(vo, user, "id", "username", "password", "status", "createTime", "updateTime", "delFlag"); + + int result = hrtUserMapper.updateById(user); + if (result <= 0) { + throw new BusinessException(500, "修改个人资料失败"); + } + + return "修改成功"; + } + + /** + * @description [更新头像] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String updateAvatar(UserAvatarUpdateVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtUser user = hrtUserMapper.selectById(userId); + if (user == null) { + throw new BusinessException(404, "用户不存在"); + } + + user.setAvatar(vo.getAvatar()); + + int result = hrtUserMapper.updateById(user); + if (result <= 0) { + throw new BusinessException(500, "更新头像失败"); + } + + return "更新成功"; + } + + /** + * @description [上传并更新头像] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String uploadAndUpdateAvatar(MultipartFile file) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + // 验证用户是否存在 + HrtUser user = hrtUserMapper.selectById(userId); + if (user == null) { + throw new BusinessException(404, "用户不存在"); + } + + // 文件验证 + validateAvatarFile(file); + + String avatarUrl; + try { + // 获取存储服务 + StorageService storageService = storageServiceFactory.getStorageService(storageType); + + // 生成唯一文件名 + String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename()); + + // 构建文件夹路径 + String folderPath = "avatars"; + + // 上传文件 + Map fileMap = storageService.uploadFile(file, fileName, folderPath); + + // 获取文件URL + avatarUrl = (String) fileMap.get("fileUploadPath"); + + log.info("用户头像上传成功: userId={}, avatarUrl={}", userId, avatarUrl); + + } catch (Exception e) { + log.error("用户头像上传失败: userId={}", userId, e); + throw new BusinessException(500, "头像上传失败: " + e.getMessage()); + } + + // 更新用户头像 + user.setAvatar(avatarUrl); + int result = hrtUserMapper.updateById(user); + if (result <= 0) { + throw new BusinessException(500, "更新头像失败"); + } + + return avatarUrl; + } + + /** + * @description [验证头像文件] + * @author Leocoder + */ + private void validateAvatarFile(MultipartFile file) { + // 检查文件是否为空 + if (file == null || file.isEmpty()) { + throw new BusinessException(400, "请选择要上传的头像文件"); + } + + // 检查文件大小 + long fileSize = file.getSize(); + if (fileSize > MAX_AVATAR_SIZE) { + throw new BusinessException(413, "头像文件大小不能超过2MB"); + } + + // 获取文件扩展名 + String originalFilename = file.getOriginalFilename(); + if (StringUtils.isBlank(originalFilename)) { + throw new BusinessException(400, "文件名不能为空"); + } + + String fileExtension = getFileExtension(originalFilename).toLowerCase(); + if (StringUtils.isBlank(fileExtension)) { + throw new BusinessException(400, "文件必须有扩展名"); + } + + // 检查文件格式 + if (!ALLOWED_AVATAR_EXTENSIONS.contains(fileExtension)) { + throw new BusinessException(400, "不支持的头像格式,仅支持:" + String.join(", ", ALLOWED_AVATAR_EXTENSIONS)); + } + + log.info("头像文件验证通过:文件名={}, 大小={}bytes, 类型={}", originalFilename, fileSize, fileExtension); + } + + /** + * @description [获取文件扩展名] + * @author Leocoder + */ + private String getFileExtension(String filename) { + if (StringUtils.isBlank(filename)) { + return ""; + } + int lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) { + return ""; + } + return filename.substring(lastDotIndex + 1); + } + + /** + * @description [修改密码] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String updatePassword(UserPasswordUpdateVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + HrtUser user = hrtUserMapper.selectById(userId); + if (user == null) { + throw new BusinessException(404, "用户不存在"); + } + + if (!vo.getNewPassword().equals(vo.getConfirmPassword())) { + throw new BusinessException(400, "两次输入的新密码不一致"); + } + + if (!CoderSaTokenPasswordUtil.matchesPassword(vo.getOldPassword(), user.getPassword())) { + throw new BusinessException(400, "旧密码不正确"); + } + + if (vo.getOldPassword().equals(vo.getNewPassword())) { + throw new BusinessException(400, "新密码不能与旧密码相同"); + } + + String encryptedPassword = CoderSaTokenPasswordUtil.encryptPassword(vo.getNewPassword()); + user.setPassword(encryptedPassword); + + int result = hrtUserMapper.updateById(user); + if (result <= 0) { + throw new BusinessException(500, "修改密码失败"); + } + + return "修改成功"; + } + + /** + * @description [获取用户统计信息] + * @author Leocoder + */ + @Override + public UserStatsVo getUserStats() { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + UserStatsVo stats = new UserStatsVo(); + + LambdaQueryWrapper viewWrapper = new LambdaQueryWrapper<>(); + viewWrapper.eq(HrtViewHistory::getUserId, userId); + stats.setViewHistoryCount(hrtViewHistoryMapper.selectCount(viewWrapper)); + + LambdaQueryWrapper commentWrapper = new LambdaQueryWrapper<>(); + commentWrapper.eq(HrtComment::getUserId, userId); + stats.setCommentCount(hrtCommentMapper.selectCount(commentWrapper)); + + LambdaQueryWrapper likeWrapper = new LambdaQueryWrapper<>(); + likeWrapper.eq(HrtLike::getUserId, userId); + stats.setLikeCount(hrtLikeMapper.selectCount(likeWrapper)); + + LambdaQueryWrapper favoriteWrapper = new LambdaQueryWrapper<>(); + favoriteWrapper.eq(HrtFavorite::getUserId, userId); + stats.setFavoriteCount(hrtFavoriteMapper.selectCount(favoriteWrapper)); + + LambdaQueryWrapper registrationWrapper = new LambdaQueryWrapper<>(); + registrationWrapper.eq(HrtEventRegistration::getUserId, userId); + stats.setEventRegistrationCount(hrtEventRegistrationMapper.selectCount(registrationWrapper)); + + return stats; + } + + /** + * @description [获取浏览历史] + * @author Leocoder + */ + @Override + public IPage getViewHistory(ViewHistoryQueryVo vo) { + Long userId = HrtStpUtil.getLoginIdAsLong(); + + Page page = new Page<>(vo.getPageNum(), vo.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtViewHistory::getUserId, userId); + + if (vo.getTargetType() != null && !vo.getTargetType().isEmpty()) { + wrapper.eq(HrtViewHistory::getTargetType, vo.getTargetType()); + } + + wrapper.orderByDesc(HrtViewHistory::getUpdateTime); + + IPage historyPage = hrtViewHistoryMapper.selectPage(page, wrapper); + + List heritageIds = historyPage.getRecords().stream() + .filter(h -> "heritage".equals(h.getTargetType())) + .map(HrtViewHistory::getTargetId) + .distinct() + .collect(Collectors.toList()); + + List inheritorIds = historyPage.getRecords().stream() + .filter(h -> "inheritor".equals(h.getTargetType())) + .map(HrtViewHistory::getTargetId) + .distinct() + .collect(Collectors.toList()); + + List newsIds = historyPage.getRecords().stream() + .filter(h -> "news".equals(h.getTargetType())) + .map(HrtViewHistory::getTargetId) + .distinct() + .collect(Collectors.toList()); + + List eventIds = historyPage.getRecords().stream() + .filter(h -> "event".equals(h.getTargetType())) + .map(HrtViewHistory::getTargetId) + .distinct() + .collect(Collectors.toList()); + + Map heritageMap = heritageIds.isEmpty() ? Map.of() : + hrtHeritageMapper.selectBatchIds(heritageIds).stream() + .collect(Collectors.toMap(HrtHeritage::getId, h -> h)); + + Map inheritorMap = inheritorIds.isEmpty() ? Map.of() : + hrtInheritorMapper.selectBatchIds(inheritorIds).stream() + .collect(Collectors.toMap(HrtInheritor::getId, i -> i)); + + Map newsMap = newsIds.isEmpty() ? Map.of() : + hrtNewsMapper.selectBatchIds(newsIds).stream() + .collect(Collectors.toMap(HrtNews::getId, n -> n)); + + Map eventMap = eventIds.isEmpty() ? Map.of() : + hrtEventMapper.selectBatchIds(eventIds).stream() + .collect(Collectors.toMap(HrtEvent::getId, e -> e)); + + Page resultPage = new Page<>(vo.getPageNum(), vo.getPageSize()); + resultPage.setTotal(historyPage.getTotal()); + resultPage.setRecords(historyPage.getRecords().stream() + .map(history -> buildViewHistoryItemVo(history, heritageMap, inheritorMap, newsMap, eventMap)) + .collect(Collectors.toList()) + ); + + return resultPage; + } + + /** + * @description [记录浏览历史] + * @author Leocoder + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void recordViewHistory(String targetType, Long targetId) { + Long userId; + try { + userId = HrtStpUtil.getLoginIdAsLong(); + } catch (Exception e) { + return; + } + + if (!TargetTypeEnum.isValid(targetType)) { + return; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(HrtViewHistory::getUserId, userId) + .eq(HrtViewHistory::getTargetType, targetType) + .eq(HrtViewHistory::getTargetId, targetId); + + HrtViewHistory existingHistory = hrtViewHistoryMapper.selectOne(wrapper); + + if (existingHistory != null) { + hrtViewHistoryMapper.updateById(existingHistory); + } else { + HrtViewHistory history = new HrtViewHistory(); + history.setUserId(userId); + history.setTargetType(targetType); + history.setTargetId(targetId); + hrtViewHistoryMapper.insert(history); + } + } + + /** + * @description [构建浏览历史项VO] + * @author Leocoder + */ + private ViewHistoryItemVo buildViewHistoryItemVo(HrtViewHistory history, + Map heritageMap, + Map inheritorMap, + Map newsMap, + Map eventMap) { + ViewHistoryItemVo itemVo = new ViewHistoryItemVo(); + itemVo.setId(history.getId()); + itemVo.setTargetType(history.getTargetType()); + itemVo.setTargetId(history.getTargetId()); + itemVo.setViewTime(history.getUpdateTime()); + + switch (history.getTargetType()) { + case "heritage": + HrtHeritage heritage = heritageMap.get(history.getTargetId()); + if (heritage != null) { + itemVo.setTargetTitle(heritage.getName()); + itemVo.setTargetCover(heritage.getCoverImage()); + itemVo.setTargetDescription(heritage.getDescription()); + } + break; + case "inheritor": + HrtInheritor inheritor = inheritorMap.get(history.getTargetId()); + if (inheritor != null) { + itemVo.setTargetTitle(inheritor.getName()); + itemVo.setTargetCover(inheritor.getAvatar()); + itemVo.setTargetDescription(inheritor.getIntroduction()); + } + break; + case "news": + HrtNews news = newsMap.get(history.getTargetId()); + if (news != null) { + itemVo.setTargetTitle(news.getTitle()); + itemVo.setTargetCover(news.getCoverImage()); + itemVo.setTargetDescription(news.getSummary()); + } + break; + case "event": + HrtEvent event = eventMap.get(history.getTargetId()); + if (event != null) { + itemVo.setTargetTitle(event.getTitle()); + itemVo.setTargetCover(event.getCoverImage()); + itemVo.setTargetDescription(event.getSummary()); + } + break; + } + + return itemVo; + } + +}