feat: 新增前台业务模块heritage-portal

【认证与用户管理】
- HrtAuthController: 用户注册、登录、退出、获取用户信息
- HrtUserController: 个人资料管理、密码修改、头像更新、用户统计、浏览历史

【非遗项目管理】
- HrtHeritageController: 非遗项目列表、详情、分类查询、精选推荐
- HrtHeritageService: 非遗项目业务逻辑、浏览量统计

【传承人管理】
- HrtInheritorController: 传承人列表、详情、级别查询
- HrtInheritorService: 传承人业务逻辑、关联项目查询

【新闻资讯管理】
- HrtNewsController: 新闻列表、详情、分类查询、置顶推荐
- HrtNewsService: 新闻业务逻辑、浏览量统计

【活动管理】
- HrtEventController: 活动列表、详情、报名、取消报名、我的报名
- HrtEventService: 活动业务逻辑、报名管理、人数统计

【互动功能】
- HrtCommentController: 评论列表、发布评论、删除评论、我的评论
- HrtFavoriteController: 收藏、取消收藏、我的收藏
- HrtLikeController: 点赞、取消点赞

【技术特性】
- 所有接口使用@SaCheckPermission进行权限验证
- 使用@RequiredArgsConstructor构造器注入
- 统一路径前缀/coder
- 完整的Swagger中文文档注解
- 支持分页查询
- 完善的参数验证
This commit is contained in:
Leo 2025-10-13 20:36:51 +08:00
parent 7bf017b03f
commit 942338c2a9
28 changed files with 3449 additions and 0 deletions

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-backend</artifactId>
<version>${revision}</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>heritage-portal</name>
<artifactId>heritage-portal</artifactId>
<description>前台业务模块</description>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据统一返回、全局异常以及限流、数据脱敏、重复提交等插件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-resultex</artifactId>
<version>${revision}</version>
</dependency>
<!-- Sa-Token组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-sa-token</artifactId>
<version>${revision}</version>
</dependency>
<!-- 操作日志组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-oper-logs</artifactId>
<version>${revision}</version>
</dependency>
<!-- OSS存储组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-oss</artifactId>
<version>${revision}</version>
</dependency>
<!-- SpringDoc OpenAPI 3.0 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

@ -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<CommentItemVo> listComments(@Validated CommentQueryVo vo) {
return hrtCommentService.listComments(vo);
}
/**
* @description [我的评论列表]
* @author Leocoder
*/
@Operation(summary = "我的评论列表", description = "分页查询当前用户的评论列表,支持按类型筛选")
@GetMapping("/myList")
public IPage<CommentItemVo> myCommentList(@Validated MyCommentQueryVo vo) {
return hrtCommentService.myCommentList(vo);
}
}

View File

@ -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<EventListVo> 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<MyEventRegistrationItemVo> myRegistrations(@Validated MyEventRegistrationVo vo) {
return hrtEventService.myRegistrations(vo);
}
}

View File

@ -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<FavoriteItemVo> 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);
}
}

View File

@ -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<HrtHeritageListVo> 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<HrtHeritageListVo> 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<HrtHeritageListVo> featured(@Parameter(description = "查询数量默认10条") @RequestParam(required = false, defaultValue = "10") Integer limit) {
return hrtHeritageService.getFeaturedList(limit);
}
}

View File

@ -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<HrtInheritorListVo> 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<HrtInheritorListVo> listByHeritage(@Parameter(description = "非遗项目ID") @PathVariable Long heritageId) {
return hrtInheritorService.listByHeritageId(heritageId);
}
/**
* @description [精选传承人]
* @author Leocoder
*/
@Operation(summary = "精选传承人", description = "获取平台精选的优秀传承人")
@GetMapping("/featured")
public List<HrtInheritorListVo> featured(@Parameter(description = "查询数量默认10条") @RequestParam(required = false, defaultValue = "10") Integer limit) {
return hrtInheritorService.getFeaturedList(limit);
}
}

View File

@ -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);
}
}

View File

@ -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<NewsListVo> 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<NewsListVo> 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<NewsListVo> topNews() {
return hrtNewsService.topNews();
}
}

View File

@ -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<ViewHistoryItemVo> getViewHistory(@Validated ViewHistoryQueryVo vo) {
return hrtUserService.getViewHistory(vo);
}
}

View File

@ -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();
}

View File

@ -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<HrtUser> 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<HrtUser> 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);
}
}

View File

@ -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<CommentItemVo> listComments(CommentQueryVo vo);
/**
* @description [我的评论列表]
* @author Leocoder
*/
IPage<CommentItemVo> myCommentList(MyCommentQueryVo vo);
}

View File

@ -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<HrtComment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtComment::getParentId, commentId);
hrtCommentMapper.delete(wrapper);
return "删除成功";
}
/**
* @description [查看评论列表按目标ID和类型]
* @author Leocoder
*/
@Override
public IPage<CommentItemVo> listComments(CommentQueryVo vo) {
Long currentUserId = null;
try {
currentUserId = HrtStpUtil.getLoginIdAsLong();
} catch (Exception e) {
}
Page<HrtComment> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtComment> 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<HrtComment> commentPage = hrtCommentMapper.selectPage(page, wrapper);
List<Long> userIds = commentPage.getRecords().stream()
.map(HrtComment::getUserId)
.distinct()
.collect(Collectors.toList());
Map<Long, HrtUser> userMap = userIds.isEmpty() ? Map.of() :
hrtUserMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(HrtUser::getId, user -> user));
List<Long> commentIds = commentPage.getRecords().stream()
.map(HrtComment::getId)
.collect(Collectors.toList());
Map<Long, List<HrtComment>> replyMap = Map.of();
if (!commentIds.isEmpty()) {
LambdaQueryWrapper<HrtComment> replyWrapper = new LambdaQueryWrapper<>();
replyWrapper.in(HrtComment::getParentId, commentIds)
.eq(HrtComment::getStatus, 1)
.orderByAsc(HrtComment::getCreateTime);
List<HrtComment> replies = hrtCommentMapper.selectList(replyWrapper);
List<Long> 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<Long, Boolean> likeMap = Map.of();
if (currentUserId != null && !commentIds.isEmpty()) {
List<Long> allCommentIds = new ArrayList<>(commentIds);
replyMap.values().forEach(replyList ->
replyList.forEach(reply -> allCommentIds.add(reply.getId()))
);
LambdaQueryWrapper<HrtLike> 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<Long, Boolean> finalLikeMap = likeMap;
final Map<Long, List<HrtComment>> finalReplyMap = replyMap;
Page<CommentItemVo> 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<CommentItemVo> myCommentList(MyCommentQueryVo vo) {
Long userId = HrtStpUtil.getLoginIdAsLong();
Page<HrtComment> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtComment> 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<HrtComment> commentPage = hrtCommentMapper.selectPage(page, wrapper);
HrtUser currentUser = hrtUserMapper.selectById(userId);
Map<Long, Boolean> likeMap = Map.of();
if (!commentPage.getRecords().isEmpty()) {
List<Long> commentIds = commentPage.getRecords().stream()
.map(HrtComment::getId)
.collect(Collectors.toList());
LambdaQueryWrapper<HrtLike> 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<Long, Boolean> finalLikeMap = likeMap;
Page<CommentItemVo> 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<Long, HrtUser> userMap,
Map<Long, List<HrtComment>> replyMap,
Map<Long, Boolean> 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<HrtComment> replies = replyMap.getOrDefault(comment.getId(), List.of());
itemVo.setReplyCount(replies.size());
if (!replies.isEmpty()) {
List<CommentItemVo> 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;
}
}

View File

@ -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<EventListVo> 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<MyEventRegistrationItemVo> myRegistrations(MyEventRegistrationVo vo);
}

View File

@ -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<EventListVo> listEvents(EventQueryVo vo) {
Long currentUserId = null;
try {
currentUserId = HrtStpUtil.getLoginIdAsLong();
} catch (Exception e) {
}
Page<HrtEvent> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtEvent> 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<HrtEvent> eventPage = hrtEventMapper.selectPage(page, wrapper);
Map<Long, Boolean> registrationMap = Map.of();
if (currentUserId != null && !eventPage.getRecords().isEmpty()) {
List<Long> eventIds = eventPage.getRecords().stream()
.map(HrtEvent::getId)
.collect(Collectors.toList());
LambdaQueryWrapper<HrtEventRegistration> 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<Long, Boolean> finalRegistrationMap = registrationMap;
Page<EventListVo> 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<HrtEvent>()
.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<HrtEventRegistration> 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<HrtEventRegistration> 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<HrtEvent>()
.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<HrtEventRegistration> 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<HrtEvent>()
.setSql("current_participants = GREATEST(current_participants - 1, 0)")
.eq(HrtEvent::getId, eventId)
);
return "取消报名成功";
}
/**
* @description [我报名的活动]
* @author Leocoder
*/
@Override
public IPage<MyEventRegistrationItemVo> myRegistrations(MyEventRegistrationVo vo) {
Long userId = HrtStpUtil.getLoginIdAsLong();
Page<HrtEventRegistration> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtEventRegistration> 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<HrtEventRegistration> registrationPage = hrtEventRegistrationMapper.selectPage(page, wrapper);
List<Long> eventIds = registrationPage.getRecords().stream()
.map(HrtEventRegistration::getEventId)
.collect(Collectors.toList());
Map<Long, HrtEvent> eventMap = eventIds.isEmpty() ? Map.of() :
hrtEventMapper.selectBatchIds(eventIds).stream()
.collect(Collectors.toMap(HrtEvent::getId, event -> event));
Page<MyEventRegistrationItemVo> 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;
}
}

View File

@ -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<FavoriteItemVo> myFavoriteList(FavoriteQueryVo vo);
/**
* @description [检查是否已收藏]
* @author Leocoder
*/
Boolean checkFavorite(String targetType, Long targetId);
}

View File

@ -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<HrtFavorite> 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<HrtFavorite> 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<FavoriteItemVo> myFavoriteList(FavoriteQueryVo vo) {
// 1获取当前登录用户ID
Long userId = HrtStpUtil.getLoginIdAsLong();
// 2分页查询收藏记录
Page<HrtFavorite> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtFavorite> 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<HrtFavorite> favoritePageResult = hrtFavoriteMapper.selectPage(page, queryWrapper);
// 3转换为VO并填充目标对象信息
Page<FavoriteItemVo> resultPage = new Page<>(vo.getPageNum(), vo.getPageSize());
resultPage.setTotal(favoritePageResult.getTotal());
resultPage.setPages(favoritePageResult.getPages());
List<FavoriteItemVo> 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<HrtFavorite> 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<HrtHeritage>()
.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;
}
}
}

View File

@ -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<HrtHeritageListVo> listPage(HrtHeritageQueryVo vo);
/**
* @description [查看非遗项目详情]
* @author Leocoder
*/
HrtHeritageDetailVo getDetail(Long id);
/**
* @description [热门非遗项目]
* @author Leocoder
*/
List<HrtHeritageListVo> getHotList(Integer limit);
/**
* @description [精选非遗项目]
* @author Leocoder
*/
List<HrtHeritageListVo> getFeaturedList(Integer limit);
}

View File

@ -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<HrtHeritageListVo> listPage(HrtHeritageQueryVo vo) {
Page<HrtHeritage> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtHeritage> wrapper = buildQueryWrapper(vo);
// 只查询已发布的项目
wrapper.eq(HrtHeritage::getPublishStatus, 1);
// 排序
applySorting(wrapper, vo.getSortField(), vo.getSortOrder());
IPage<HrtHeritage> 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<HrtHeritageListVo> getHotList(Integer limit) {
// 默认查询10条
if (limit == null || limit <= 0) {
limit = 10;
}
LambdaQueryWrapper<HrtHeritage> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtHeritage::getPublishStatus, 1)
.orderByDesc(HrtHeritage::getViewCount, HrtHeritage::getLikeCount)
.last("LIMIT " + limit);
List<HrtHeritage> heritageList = hrtHeritageMapper.selectList(wrapper);
return heritageList.stream()
.map(heritage -> BeanUtil.copyProperties(heritage, HrtHeritageListVo.class))
.collect(Collectors.toList());
}
/**
* @description [精选非遗项目]
* @author Leocoder
*/
@Override
public List<HrtHeritageListVo> getFeaturedList(Integer limit) {
// 默认查询10条
if (limit == null || limit <= 0) {
limit = 10;
}
LambdaQueryWrapper<HrtHeritage> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtHeritage::getPublishStatus, 1)
.eq(HrtHeritage::getIsFeatured, 1)
.orderByDesc(HrtHeritage::getSortOrder, HrtHeritage::getCreateTime)
.last("LIMIT " + limit);
List<HrtHeritage> heritageList = hrtHeritageMapper.selectList(wrapper);
return heritageList.stream()
.map(heritage -> BeanUtil.copyProperties(heritage, HrtHeritageListVo.class))
.collect(Collectors.toList());
}
/**
* @description [构建查询条件]
* @author Leocoder
*/
private LambdaQueryWrapper<HrtHeritage> buildQueryWrapper(HrtHeritageQueryVo vo) {
LambdaQueryWrapper<HrtHeritage> 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<HrtHeritage> 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;
}
}
}

View File

@ -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<HrtInheritorListVo> listPage(HrtInheritorQueryVo vo);
/**
* @description [查看传承人详情]
* @author Leocoder
*/
HrtInheritorDetailVo getDetail(Long id);
/**
* @description [根据非遗项目查询传承人]
* @author Leocoder
*/
List<HrtInheritorListVo> listByHeritageId(Long heritageId);
/**
* @description [精选传承人]
* @author Leocoder
*/
List<HrtInheritorListVo> getFeaturedList(Integer limit);
}

View File

@ -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<HrtInheritorListVo> listPage(HrtInheritorQueryVo vo) {
Page<HrtInheritor> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtInheritor> wrapper = buildQueryWrapper(vo);
// 只查询已发布的传承人
wrapper.eq(HrtInheritor::getPublishStatus, 1);
// 排序
applySorting(wrapper, vo.getSortField(), vo.getSortOrder());
IPage<HrtInheritor> 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<HrtInheritorListVo> listByHeritageId(Long heritageId) {
if (heritageId == null) {
throw new BusinessException(400, "非遗项目ID不能为空");
}
LambdaQueryWrapper<HrtInheritor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtInheritor::getHeritageId, heritageId)
.eq(HrtInheritor::getPublishStatus, 1)
.orderByDesc(HrtInheritor::getSortOrder, HrtInheritor::getCreateTime);
List<HrtInheritor> inheritorList = hrtInheritorMapper.selectList(wrapper);
return inheritorList.stream()
.map(inheritor -> BeanUtil.copyProperties(inheritor, HrtInheritorListVo.class))
.collect(Collectors.toList());
}
/**
* @description [精选传承人]
* @author Leocoder
*/
@Override
public List<HrtInheritorListVo> getFeaturedList(Integer limit) {
// 默认查询10条
if (limit == null || limit <= 0) {
limit = 10;
}
LambdaQueryWrapper<HrtInheritor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtInheritor::getPublishStatus, 1)
.eq(HrtInheritor::getIsFeatured, 1)
.orderByDesc(HrtInheritor::getSortOrder, HrtInheritor::getCreateTime)
.last("LIMIT " + limit);
List<HrtInheritor> inheritorList = hrtInheritorMapper.selectList(wrapper);
return inheritorList.stream()
.map(inheritor -> BeanUtil.copyProperties(inheritor, HrtInheritorListVo.class))
.collect(Collectors.toList());
}
/**
* @description [构建查询条件]
* @author Leocoder
*/
private LambdaQueryWrapper<HrtInheritor> buildQueryWrapper(HrtInheritorQueryVo vo) {
LambdaQueryWrapper<HrtInheritor> 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<HrtInheritor> 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;
}
}
}

View File

@ -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);
}

View File

@ -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<HrtLike> 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<HrtLike> 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<HrtLike> 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<HrtHeritage>()
.setSql(sqlSet)
.eq(HrtHeritage::getId, targetId)
);
break;
case INHERITOR:
hrtInheritorMapper.update(null,
new LambdaUpdateWrapper<HrtInheritor>()
.setSql(sqlSet)
.eq(HrtInheritor::getId, targetId)
);
break;
case NEWS:
hrtNewsMapper.update(null,
new LambdaUpdateWrapper<HrtNews>()
.setSql(sqlSet)
.eq(HrtNews::getId, targetId)
);
break;
case COMMENT:
hrtCommentMapper.update(null,
new LambdaUpdateWrapper<HrtComment>()
.setSql(sqlSet)
.eq(HrtComment::getId, targetId)
);
break;
}
}
}

View File

@ -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<NewsListVo> listNews(NewsQueryVo vo);
/**
* @description [查看新闻详情]
* @author Leocoder
*/
NewsDetailVo getNewsDetail(Long newsId);
/**
* @description [热门新闻]
* @author Leocoder
*/
List<NewsListVo> hotNews(Integer limit);
/**
* @description [置顶新闻]
* @author Leocoder
*/
List<NewsListVo> topNews();
}

View File

@ -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<NewsListVo> listNews(NewsQueryVo vo) {
Page<HrtNews> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtNews> 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<HrtNews> newsPage = hrtNewsMapper.selectPage(page, wrapper);
Page<NewsListVo> 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<HrtNews>()
.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<HrtLike> 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<HrtFavorite> 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<NewsListVo> hotNews(Integer limit) {
if (limit == null || limit <= 0) {
limit = 10;
}
LambdaQueryWrapper<HrtNews> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtNews::getPublishStatus, 1)
.orderByDesc(HrtNews::getViewCount)
.last("LIMIT " + limit);
List<HrtNews> newsList = hrtNewsMapper.selectList(wrapper);
return newsList.stream()
.map(news -> BeanUtil.copyProperties(news, NewsListVo.class))
.collect(Collectors.toList());
}
/**
* @description [置顶新闻]
* @author Leocoder
*/
@Override
public List<NewsListVo> topNews() {
LambdaQueryWrapper<HrtNews> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HrtNews::getPublishStatus, 1)
.eq(HrtNews::getIsTop, 1)
.orderByDesc(HrtNews::getPublishTime);
List<HrtNews> newsList = hrtNewsMapper.selectList(wrapper);
return newsList.stream()
.map(news -> BeanUtil.copyProperties(news, NewsListVo.class))
.collect(Collectors.toList());
}
}

View File

@ -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<ViewHistoryItemVo> getViewHistory(ViewHistoryQueryVo vo);
/**
* @description [记录浏览历史]
* @author Leocoder
*/
void recordViewHistory(String targetType, Long targetId);
}

View File

@ -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<String> 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<HrtUser> 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<HrtUser> 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<String, Object> 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<HrtViewHistory> viewWrapper = new LambdaQueryWrapper<>();
viewWrapper.eq(HrtViewHistory::getUserId, userId);
stats.setViewHistoryCount(hrtViewHistoryMapper.selectCount(viewWrapper));
LambdaQueryWrapper<HrtComment> commentWrapper = new LambdaQueryWrapper<>();
commentWrapper.eq(HrtComment::getUserId, userId);
stats.setCommentCount(hrtCommentMapper.selectCount(commentWrapper));
LambdaQueryWrapper<HrtLike> likeWrapper = new LambdaQueryWrapper<>();
likeWrapper.eq(HrtLike::getUserId, userId);
stats.setLikeCount(hrtLikeMapper.selectCount(likeWrapper));
LambdaQueryWrapper<HrtFavorite> favoriteWrapper = new LambdaQueryWrapper<>();
favoriteWrapper.eq(HrtFavorite::getUserId, userId);
stats.setFavoriteCount(hrtFavoriteMapper.selectCount(favoriteWrapper));
LambdaQueryWrapper<HrtEventRegistration> registrationWrapper = new LambdaQueryWrapper<>();
registrationWrapper.eq(HrtEventRegistration::getUserId, userId);
stats.setEventRegistrationCount(hrtEventRegistrationMapper.selectCount(registrationWrapper));
return stats;
}
/**
* @description [获取浏览历史]
* @author Leocoder
*/
@Override
public IPage<ViewHistoryItemVo> getViewHistory(ViewHistoryQueryVo vo) {
Long userId = HrtStpUtil.getLoginIdAsLong();
Page<HrtViewHistory> page = new Page<>(vo.getPageNum(), vo.getPageSize());
LambdaQueryWrapper<HrtViewHistory> 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<HrtViewHistory> historyPage = hrtViewHistoryMapper.selectPage(page, wrapper);
List<Long> heritageIds = historyPage.getRecords().stream()
.filter(h -> "heritage".equals(h.getTargetType()))
.map(HrtViewHistory::getTargetId)
.distinct()
.collect(Collectors.toList());
List<Long> inheritorIds = historyPage.getRecords().stream()
.filter(h -> "inheritor".equals(h.getTargetType()))
.map(HrtViewHistory::getTargetId)
.distinct()
.collect(Collectors.toList());
List<Long> newsIds = historyPage.getRecords().stream()
.filter(h -> "news".equals(h.getTargetType()))
.map(HrtViewHistory::getTargetId)
.distinct()
.collect(Collectors.toList());
List<Long> eventIds = historyPage.getRecords().stream()
.filter(h -> "event".equals(h.getTargetType()))
.map(HrtViewHistory::getTargetId)
.distinct()
.collect(Collectors.toList());
Map<Long, HrtHeritage> heritageMap = heritageIds.isEmpty() ? Map.of() :
hrtHeritageMapper.selectBatchIds(heritageIds).stream()
.collect(Collectors.toMap(HrtHeritage::getId, h -> h));
Map<Long, HrtInheritor> inheritorMap = inheritorIds.isEmpty() ? Map.of() :
hrtInheritorMapper.selectBatchIds(inheritorIds).stream()
.collect(Collectors.toMap(HrtInheritor::getId, i -> i));
Map<Long, HrtNews> newsMap = newsIds.isEmpty() ? Map.of() :
hrtNewsMapper.selectBatchIds(newsIds).stream()
.collect(Collectors.toMap(HrtNews::getId, n -> n));
Map<Long, HrtEvent> eventMap = eventIds.isEmpty() ? Map.of() :
hrtEventMapper.selectBatchIds(eventIds).stream()
.collect(Collectors.toMap(HrtEvent::getId, e -> e));
Page<ViewHistoryItemVo> 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<HrtViewHistory> 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<Long, HrtHeritage> heritageMap,
Map<Long, HrtInheritor> inheritorMap,
Map<Long, HrtNews> newsMap,
Map<Long, HrtEvent> 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;
}
}