feat(backend): Improve authorization governance and video interactions
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
||||
package com.nuaa.video.admin.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.admin.annotation.OperationLog;
|
||||
import com.nuaa.video.admin.dto.AdminRoleDTO;
|
||||
import com.nuaa.video.admin.service.AdminPermissionService;
|
||||
import com.nuaa.video.admin.service.AdminRoleService;
|
||||
import com.nuaa.video.admin.vo.AdminPermissionVO;
|
||||
import com.nuaa.video.admin.vo.AdminRoleVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色和权限管理控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/role")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminRoleController {
|
||||
|
||||
private final AdminRoleService adminRoleService;
|
||||
private final AdminPermissionService adminPermissionService;
|
||||
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/list")
|
||||
public Result<Page<AdminRoleVO>> listRoles(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
return adminRoleService.listRoles(new Page<>(page, size), keyword);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/all")
|
||||
public Result<List<AdminRoleVO>> listAllRoles() {
|
||||
return adminRoleService.listAllRoles();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<AdminRoleVO> getRoleDetail(@PathVariable Long id) {
|
||||
return adminRoleService.getRoleDetail(id);
|
||||
}
|
||||
|
||||
@OperationLog("新增角色")
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PostMapping
|
||||
public Result<Void> addRole(@RequestBody @Valid AdminRoleDTO roleDTO) {
|
||||
return adminRoleService.addRole(roleDTO, getCurrentUsername());
|
||||
}
|
||||
|
||||
@OperationLog("更新角色")
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PutMapping
|
||||
public Result<Void> updateRole(@RequestBody @Valid AdminRoleDTO roleDTO) {
|
||||
return adminRoleService.updateRole(roleDTO, getCurrentUsername());
|
||||
}
|
||||
|
||||
@OperationLog("角色授权")
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PutMapping("/{id}/permissions")
|
||||
public Result<Void> updateRolePermissions(@PathVariable Long id, @RequestBody List<Long> permissionIds) {
|
||||
return adminRoleService.updateRolePermissions(id, permissionIds, getCurrentUsername());
|
||||
}
|
||||
|
||||
@OperationLog("删除角色")
|
||||
@PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteRole(@PathVariable Long id) {
|
||||
return adminRoleService.deleteRole(id);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('permission:manage') or hasAuthority('role:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/permissions/list")
|
||||
public Result<List<AdminPermissionVO>> listPermissions() {
|
||||
return adminPermissionService.listPermissions();
|
||||
}
|
||||
|
||||
private String getCurrentUsername() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return authentication.getName();
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -29,7 +29,7 @@ public class AdminUserController {
|
||||
/**
|
||||
* 获取管理员列表
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/list")
|
||||
public Result<Page<AdminUserVO>> listUsers(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@@ -42,7 +42,7 @@ public class AdminUserController {
|
||||
/**
|
||||
* 获取管理员详情
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<AdminUserVO> getUser(@PathVariable Long id) {
|
||||
return adminUserService.getUserDetail(id);
|
||||
@@ -52,7 +52,7 @@ public class AdminUserController {
|
||||
* 添加管理员
|
||||
*/
|
||||
@OperationLog("添加管理员")
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PostMapping
|
||||
public Result<Void> addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
||||
return adminUserService.addUser(adminUserDTO, getCurrentUsername());
|
||||
@@ -62,7 +62,7 @@ public class AdminUserController {
|
||||
* 更新管理员
|
||||
*/
|
||||
@OperationLog("更新管理员")
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PutMapping
|
||||
public Result<Void> updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
||||
return adminUserService.updateUser(adminUserDTO, getCurrentUsername());
|
||||
@@ -72,7 +72,7 @@ public class AdminUserController {
|
||||
* 删除管理员
|
||||
*/
|
||||
@OperationLog("删除管理员")
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteUser(@PathVariable Long id) {
|
||||
return adminUserService.deleteUser(id);
|
||||
@@ -82,7 +82,7 @@ public class AdminUserController {
|
||||
* 启用/禁用管理员
|
||||
*/
|
||||
@OperationLog("修改管理员状态")
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PutMapping("/{id}/status/{status}")
|
||||
public Result<Void> updateStatus(@PathVariable Long id, @PathVariable Integer status) {
|
||||
return adminUserService.updateStatus(id, status, getCurrentUsername());
|
||||
@@ -92,7 +92,7 @@ public class AdminUserController {
|
||||
* 重置密码
|
||||
*/
|
||||
@OperationLog("重置管理员密码")
|
||||
@PreAuthorize("hasAuthority('user:manage')")
|
||||
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||
@PutMapping("/{id}/reset-password")
|
||||
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
|
||||
return adminUserService.resetPassword(id, newPassword, getCurrentUsername());
|
||||
@@ -108,4 +108,4 @@ public class AdminUserController {
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -34,8 +34,9 @@ public class CommentManageController {
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
return commentFeignClient.getCommentList(page, size, videoId, userId, keyword, internalServiceToken);
|
||||
return commentFeignClient.getCommentList(page, size, videoId, userId, status, keyword, internalServiceToken);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,6 +58,16 @@ public class CommentManageController {
|
||||
return commentFeignClient.deleteComment(commentId, internalServiceToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复评论
|
||||
*/
|
||||
@OperationLog("恢复评论")
|
||||
@PreAuthorize("hasAuthority('comment:manage')")
|
||||
@PutMapping("/{commentId}/restore")
|
||||
public Result<Void> restoreComment(@PathVariable Long commentId) {
|
||||
return commentFeignClient.restoreComment(commentId, internalServiceToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论统计信息
|
||||
*/
|
||||
|
||||
+3
-2
@@ -32,8 +32,9 @@ public class UserManageController {
|
||||
public Result<Page<Object>> getUserList(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
return userFeignClient.getUserList(page, size, keyword, internalServiceToken);
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "status", required = false) Integer status) {
|
||||
return userFeignClient.getUserList(page, size, keyword, status, internalServiceToken);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.nuaa.video.admin.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -29,7 +28,6 @@ public class AdminRoleDTO implements Serializable {
|
||||
* 角色编码
|
||||
*/
|
||||
@NotBlank(message = "角色编码不能为空")
|
||||
@Pattern(regexp = "^ADMIN_[A-Z_]+$", message = "角色编码必须以ADMIN_开头")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
@@ -38,7 +36,7 @@ public class AdminRoleDTO implements Serializable {
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 权限ID集合
|
||||
* 权限ID列表
|
||||
*/
|
||||
private List<Long> permissionIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public interface CommentFeignClient {
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
@@ -37,6 +38,13 @@ public interface CommentFeignClient {
|
||||
Result<Void> deleteComment(@PathVariable("commentId") Long commentId,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 恢复评论
|
||||
*/
|
||||
@PutMapping("/comment/admin/{commentId}/restore")
|
||||
Result<Void> restoreComment(@PathVariable("commentId") Long commentId,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 获取评论统计信息
|
||||
*/
|
||||
|
||||
+6
-1
@@ -19,7 +19,7 @@ public class CommentFeignClientFallbackFactory implements FallbackFactory<Commen
|
||||
|
||||
return new CommentFeignClient() {
|
||||
@Override
|
||||
public Result<Page<Object>> getCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword, String internalToken) {
|
||||
public Result<Page<Object>> getCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword, String internalToken) {
|
||||
return Result.error("获取评论列表失败:服务不可用");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ public class CommentFeignClientFallbackFactory implements FallbackFactory<Commen
|
||||
return Result.error("删除评论失败:服务不可用");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> restoreComment(Long commentId, String internalToken) {
|
||||
return Result.error("恢复评论失败:服务不可用");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Object> getCommentStats(String internalToken) {
|
||||
return Result.error("获取评论统计信息失败:服务不可用");
|
||||
|
||||
@@ -19,6 +19,7 @@ public interface UserFeignClient {
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
||||
log.error("用户服务调用失败:", cause);
|
||||
return new UserFeignClient() {
|
||||
@Override
|
||||
public Result<Page<Object>> getUserList(Integer page, Integer size, String keyword, String internalToken) {
|
||||
public Result<Page<Object>> getUserList(Integer page, Integer size, String keyword, Integer status, String internalToken) {
|
||||
log.error("获取用户列表失败");
|
||||
return Result.error("获取用户列表失败,用户服务不可用");
|
||||
}
|
||||
|
||||
+60
-1
@@ -1,5 +1,9 @@
|
||||
package com.nuaa.video.admin.security;
|
||||
|
||||
import com.nuaa.video.admin.entity.AdminPermission;
|
||||
import com.nuaa.video.admin.entity.AdminRole;
|
||||
import com.nuaa.video.admin.entity.AdminUser;
|
||||
import com.nuaa.video.admin.service.AdminUserService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -14,7 +18,10 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -25,7 +32,21 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final List<String> SUPER_ADMIN_AUTHORITIES = List.of(
|
||||
"user:manage",
|
||||
"role:manage",
|
||||
"permission:manage",
|
||||
"video:manage",
|
||||
"comment:manage",
|
||||
"tag:manage",
|
||||
"category:manage",
|
||||
"recommend:manage",
|
||||
"system:config",
|
||||
"operation:log"
|
||||
);
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final AdminUserService adminUserService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
@@ -39,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
// 从令牌中获取用户信息
|
||||
Long adminId = jwtTokenProvider.getAdminIdFromToken(token);
|
||||
String username = jwtTokenProvider.getUsernameFromToken(token);
|
||||
List<String> authorities = jwtTokenProvider.getAuthoritiesFromToken(token);
|
||||
List<String> authorities = resolveAuthorities(username, token);
|
||||
|
||||
// 转换为Spring Security的授权对象
|
||||
List<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
|
||||
@@ -72,6 +93,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private List<String> resolveAuthorities(String username, String token) {
|
||||
AdminUser adminUser = adminUserService.getByUsername(username);
|
||||
if (adminUser != null) {
|
||||
Set<String> authorities = new LinkedHashSet<>();
|
||||
if (adminUser.getRoles() != null) {
|
||||
for (AdminRole role : adminUser.getRoles()) {
|
||||
if (role == null || !StringUtils.hasText(role.getCode())) {
|
||||
continue;
|
||||
}
|
||||
authorities.add("ROLE_" + role.getCode());
|
||||
if ("ADMIN_SUPER".equals(role.getCode())) {
|
||||
authorities.addAll(SUPER_ADMIN_AUTHORITIES);
|
||||
}
|
||||
if (role.getPermissions() != null) {
|
||||
for (AdminPermission permission : role.getPermissions()) {
|
||||
if (permission != null && StringUtils.hasText(permission.getCode())) {
|
||||
authorities.add(permission.getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!authorities.isEmpty()) {
|
||||
return new ArrayList<>(authorities);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> tokenAuthorities = new ArrayList<>(jwtTokenProvider.getAuthoritiesFromToken(token));
|
||||
if (tokenAuthorities.contains("ROLE_ADMIN_SUPER")) {
|
||||
SUPER_ADMIN_AUTHORITIES.forEach(authority -> {
|
||||
if (!tokenAuthorities.contains(authority)) {
|
||||
tokenAuthorities.add(authority);
|
||||
}
|
||||
});
|
||||
}
|
||||
return tokenAuthorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*/
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.nuaa.video.admin.service;
|
||||
|
||||
import com.nuaa.video.admin.vo.AdminPermissionVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限服务
|
||||
*/
|
||||
public interface AdminPermissionService {
|
||||
|
||||
Result<List<AdminPermissionVO>> listPermissions();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.nuaa.video.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.admin.dto.AdminRoleDTO;
|
||||
import com.nuaa.video.admin.vo.AdminRoleVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色服务
|
||||
*/
|
||||
public interface AdminRoleService {
|
||||
|
||||
Result<Page<AdminRoleVO>> listRoles(Page<?> page, String keyword);
|
||||
|
||||
Result<List<AdminRoleVO>> listAllRoles();
|
||||
|
||||
Result<AdminRoleVO> getRoleDetail(Long id);
|
||||
|
||||
Result<Void> addRole(AdminRoleDTO roleDTO, String operator);
|
||||
|
||||
Result<Void> updateRole(AdminRoleDTO roleDTO, String operator);
|
||||
|
||||
Result<Void> updateRolePermissions(Long roleId, List<Long> permissionIds, String operator);
|
||||
|
||||
Result<Void> deleteRole(Long id);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.nuaa.video.admin.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.nuaa.video.admin.entity.AdminPermission;
|
||||
import com.nuaa.video.admin.mapper.AdminPermissionMapper;
|
||||
import com.nuaa.video.admin.service.AdminPermissionService;
|
||||
import com.nuaa.video.admin.vo.AdminPermissionVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 权限服务实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AdminPermissionServiceImpl implements AdminPermissionService {
|
||||
|
||||
private final AdminPermissionMapper adminPermissionMapper;
|
||||
|
||||
@Override
|
||||
public Result<List<AdminPermissionVO>> listPermissions() {
|
||||
List<AdminPermission> permissions = adminPermissionMapper.selectList(new LambdaQueryWrapper<AdminPermission>()
|
||||
.orderByAsc(AdminPermission::getId));
|
||||
return Result.success(permissions.stream().map(this::toPermissionVO).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private AdminPermissionVO toPermissionVO(AdminPermission permission) {
|
||||
AdminPermissionVO permissionVO = new AdminPermissionVO();
|
||||
BeanUtils.copyProperties(permission, permissionVO);
|
||||
return permissionVO;
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package com.nuaa.video.admin.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.admin.dto.AdminRoleDTO;
|
||||
import com.nuaa.video.admin.entity.AdminPermission;
|
||||
import com.nuaa.video.admin.entity.AdminRole;
|
||||
import com.nuaa.video.admin.entity.AdminRolePermission;
|
||||
import com.nuaa.video.admin.entity.AdminUserRole;
|
||||
import com.nuaa.video.admin.mapper.AdminRoleMapper;
|
||||
import com.nuaa.video.admin.mapper.AdminRolePermissionMapper;
|
||||
import com.nuaa.video.admin.mapper.AdminUserRoleMapper;
|
||||
import com.nuaa.video.admin.service.AdminRoleService;
|
||||
import com.nuaa.video.admin.vo.AdminPermissionVO;
|
||||
import com.nuaa.video.admin.vo.AdminRoleVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 角色服务实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AdminRoleServiceImpl implements AdminRoleService {
|
||||
|
||||
private final AdminRoleMapper adminRoleMapper;
|
||||
private final AdminRolePermissionMapper adminRolePermissionMapper;
|
||||
private final AdminUserRoleMapper adminUserRoleMapper;
|
||||
|
||||
@Override
|
||||
public Result<Page<AdminRoleVO>> listRoles(Page<?> page, String keyword) {
|
||||
Page<AdminRole> rolePage = adminRoleMapper.selectRoleList(new Page<>(page.getCurrent(), page.getSize()), keyword);
|
||||
Page<AdminRoleVO> result = new Page<>();
|
||||
BeanUtils.copyProperties(rolePage, result, "records");
|
||||
result.setRecords(rolePage.getRecords().stream().map(this::toRoleVO).collect(Collectors.toList()));
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<AdminRoleVO>> listAllRoles() {
|
||||
List<AdminRole> roles = adminRoleMapper.selectRoleList(new Page<>(1, 200), null).getRecords();
|
||||
return Result.success(roles.stream().map(this::toRoleVO).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<AdminRoleVO> getRoleDetail(Long id) {
|
||||
AdminRole role = adminRoleMapper.selectRoleDetail(id);
|
||||
if (role == null) {
|
||||
return Result.error("角色不存在");
|
||||
}
|
||||
return Result.success(toRoleVO(role));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> addRole(AdminRoleDTO roleDTO, String operator) {
|
||||
if (existsRoleName(roleDTO.getName(), null)) {
|
||||
return Result.error("角色名称已存在");
|
||||
}
|
||||
if (existsRoleCode(roleDTO.getCode(), null)) {
|
||||
return Result.error("角色编码已存在");
|
||||
}
|
||||
|
||||
AdminRole role = new AdminRole();
|
||||
role.setName(roleDTO.getName());
|
||||
role.setCode(roleDTO.getCode());
|
||||
role.setDescription(roleDTO.getDescription());
|
||||
role.setCreatedBy(operator);
|
||||
role.setUpdatedBy(operator);
|
||||
role.setCreateTime(LocalDateTime.now());
|
||||
role.setUpdateTime(LocalDateTime.now());
|
||||
adminRoleMapper.insert(role);
|
||||
|
||||
saveRolePermissions(role.getId(), roleDTO.getPermissionIds(), operator);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> updateRole(AdminRoleDTO roleDTO, String operator) {
|
||||
AdminRole existing = adminRoleMapper.selectById(roleDTO.getId());
|
||||
if (existing == null) {
|
||||
return Result.error("角色不存在");
|
||||
}
|
||||
if (existsRoleName(roleDTO.getName(), roleDTO.getId())) {
|
||||
return Result.error("角色名称已存在");
|
||||
}
|
||||
if (existsRoleCode(roleDTO.getCode(), roleDTO.getId())) {
|
||||
return Result.error("角色编码已存在");
|
||||
}
|
||||
|
||||
AdminRole role = new AdminRole();
|
||||
role.setId(roleDTO.getId());
|
||||
role.setName(roleDTO.getName());
|
||||
role.setCode(roleDTO.getCode());
|
||||
role.setDescription(roleDTO.getDescription());
|
||||
role.setUpdatedBy(operator);
|
||||
role.setUpdateTime(LocalDateTime.now());
|
||||
adminRoleMapper.updateById(role);
|
||||
|
||||
if (roleDTO.getPermissionIds() != null) {
|
||||
replaceRolePermissions(roleDTO.getId(), roleDTO.getPermissionIds(), operator);
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> updateRolePermissions(Long roleId, List<Long> permissionIds, String operator) {
|
||||
AdminRole existing = adminRoleMapper.selectById(roleId);
|
||||
if (existing == null) {
|
||||
return Result.error("角色不存在");
|
||||
}
|
||||
replaceRolePermissions(roleId, permissionIds, operator);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteRole(Long id) {
|
||||
AdminRole existing = adminRoleMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
return Result.error("角色不存在");
|
||||
}
|
||||
|
||||
long bindCount = adminUserRoleMapper.selectCount(new LambdaQueryWrapper<AdminUserRole>()
|
||||
.eq(AdminUserRole::getRoleId, id));
|
||||
if (bindCount > 0) {
|
||||
return Result.error("该角色已分配给管理员,无法删除");
|
||||
}
|
||||
|
||||
adminRolePermissionMapper.delete(new LambdaQueryWrapper<AdminRolePermission>()
|
||||
.eq(AdminRolePermission::getRoleId, id));
|
||||
adminRoleMapper.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private boolean existsRoleName(String name, Long excludeId) {
|
||||
return adminRoleMapper.selectCount(new LambdaQueryWrapper<AdminRole>()
|
||||
.eq(AdminRole::getName, name)
|
||||
.ne(excludeId != null, AdminRole::getId, excludeId)) > 0;
|
||||
}
|
||||
|
||||
private boolean existsRoleCode(String code, Long excludeId) {
|
||||
return adminRoleMapper.selectCount(new LambdaQueryWrapper<AdminRole>()
|
||||
.eq(AdminRole::getCode, code)
|
||||
.ne(excludeId != null, AdminRole::getId, excludeId)) > 0;
|
||||
}
|
||||
|
||||
private void replaceRolePermissions(Long roleId, List<Long> permissionIds, String operator) {
|
||||
adminRolePermissionMapper.delete(new LambdaQueryWrapper<AdminRolePermission>()
|
||||
.eq(AdminRolePermission::getRoleId, roleId));
|
||||
saveRolePermissions(roleId, permissionIds, operator);
|
||||
}
|
||||
|
||||
private void saveRolePermissions(Long roleId, List<Long> permissionIds, String operator) {
|
||||
if (permissionIds == null || permissionIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> validPermissionIds = permissionIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!validPermissionIds.isEmpty()) {
|
||||
adminRolePermissionMapper.batchInsert(roleId, validPermissionIds, operator);
|
||||
}
|
||||
}
|
||||
|
||||
private AdminRoleVO toRoleVO(AdminRole role) {
|
||||
AdminRoleVO roleVO = new AdminRoleVO();
|
||||
BeanUtils.copyProperties(role, roleVO);
|
||||
List<AdminPermission> permissions = role.getPermissions() == null ? Collections.emptyList() :
|
||||
role.getPermissions().stream().filter(item -> item.getId() != null).collect(Collectors.toList());
|
||||
roleVO.setPermissions(permissions.stream().map(this::toPermissionVO).collect(Collectors.toList()));
|
||||
roleVO.setPermissionIds(permissions.stream().map(AdminPermission::getId).collect(Collectors.toList()));
|
||||
return roleVO;
|
||||
}
|
||||
|
||||
private AdminPermissionVO toPermissionVO(AdminPermission permission) {
|
||||
AdminPermissionVO permissionVO = new AdminPermissionVO();
|
||||
BeanUtils.copyProperties(permission, permissionVO);
|
||||
return permissionVO;
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -53,14 +53,31 @@ public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser
|
||||
|
||||
@Override
|
||||
public Result<Page<AdminUserVO>> listUsers(Page<AdminUser> page, String keyword) {
|
||||
Page<AdminUser> userPage = adminUserMapper.selectUserList(page, keyword);
|
||||
LambdaQueryWrapper<AdminUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.like(AdminUser::getUsername, keyword)
|
||||
.or()
|
||||
.like(AdminUser::getRealName, keyword)
|
||||
.or()
|
||||
.like(AdminUser::getEmail, keyword)
|
||||
.or()
|
||||
.like(AdminUser::getPhone, keyword));
|
||||
}
|
||||
queryWrapper.orderByAsc(AdminUser::getId);
|
||||
|
||||
Page<AdminUser> userPage = page(page, queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
Page<AdminUserVO> voPage = new Page<>();
|
||||
BeanUtils.copyProperties(userPage, voPage, "records");
|
||||
|
||||
List<AdminUserVO> userVOList = new ArrayList<>();
|
||||
for (AdminUser user : userPage.getRecords()) {
|
||||
for (AdminUser baseUser : userPage.getRecords()) {
|
||||
AdminUser user = adminUserMapper.selectUserDetail(baseUser.getId());
|
||||
if (user == null) {
|
||||
user = baseUser;
|
||||
}
|
||||
AdminUserVO vo = new AdminUserVO();
|
||||
BeanUtils.copyProperties(user, vo);
|
||||
|
||||
|
||||
+3
-1
@@ -7,6 +7,7 @@ import com.nuaa.video.admin.dto.LoginDTO;
|
||||
import com.nuaa.video.admin.entity.AdminPermission;
|
||||
import com.nuaa.video.admin.entity.AdminRole;
|
||||
import com.nuaa.video.admin.entity.AdminUser;
|
||||
import com.nuaa.video.admin.mapper.AdminUserMapper;
|
||||
import com.nuaa.video.admin.security.AdminUserDetails;
|
||||
import com.nuaa.video.admin.security.JwtTokenProvider;
|
||||
import com.nuaa.video.admin.service.AdminUserService;
|
||||
@@ -44,6 +45,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final AdminUserService adminUserService;
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Value("${login.retry-count:5}")
|
||||
@@ -169,7 +171,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
@Override
|
||||
public Result<LoginVO> getCurrentUser(AdminUserDetails adminUserDetails) {
|
||||
// 获取用户信息
|
||||
AdminUser adminUser = adminUserService.getById(adminUserDetails.getId());
|
||||
AdminUser adminUser = adminUserMapper.selectUserDetail(adminUserDetails.getId());
|
||||
if (adminUser == null) {
|
||||
return Result.error("用户不存在");
|
||||
}
|
||||
|
||||
+24
@@ -1,5 +1,6 @@
|
||||
package com.nuaa.video.auth.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.auth.entity.AuthUser;
|
||||
import com.nuaa.video.auth.service.AuthUserService;
|
||||
@@ -10,6 +11,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认证管理控制器 - 供管理后台调用
|
||||
*/
|
||||
@@ -63,6 +67,26 @@ public class AdminAuthController {
|
||||
return authUserService.lockUser(userId, locked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按账号状态查询用户ID - 管理接口
|
||||
*/
|
||||
@GetMapping("/user-ids/status/{status}")
|
||||
public Result<List<Long>> getUserIdsByStatus(
|
||||
@PathVariable Integer status,
|
||||
HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
|
||||
List<Long> userIds = authUserService.list(new LambdaQueryWrapper<AuthUser>()
|
||||
.select(AuthUser::getId)
|
||||
.eq(AuthUser::getStatus, status))
|
||||
.stream()
|
||||
.map(AuthUser::getId)
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(userIds);
|
||||
}
|
||||
|
||||
private boolean isInternalRequest(HttpServletRequest request) {
|
||||
return internalServiceToken.equals(request.getHeader("X-Internal-Token"));
|
||||
}
|
||||
|
||||
+15
-2
@@ -3,6 +3,7 @@ package com.nuaa.video.comment.controller;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.service.CommentService;
|
||||
import com.nuaa.video.comment.vo.AdminCommentVO;
|
||||
import com.nuaa.video.comment.vo.CommentStatVO;
|
||||
import com.nuaa.video.comment.vo.CommentVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
@@ -30,18 +31,19 @@ public class AdminCommentController {
|
||||
* 获取评论列表 - 管理接口
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<Page<CommentVO>> getCommentList(
|
||||
public Result<Page<AdminCommentVO>> getCommentList(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
|
||||
return commentService.getAdminCommentList(page, size, videoId, userId, keyword);
|
||||
return commentService.getAdminCommentList(page, size, videoId, userId, status, keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +68,17 @@ public class AdminCommentController {
|
||||
return commentService.adminDeleteComment(commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复评论 - 管理接口
|
||||
*/
|
||||
@PutMapping("/{commentId}/restore")
|
||||
public Result<Void> restoreComment(@PathVariable Long commentId, HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
return commentService.adminRestoreComment(commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论统计信息 - 管理接口
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端
|
||||
@@ -35,5 +36,7 @@ public interface UserFeignClient {
|
||||
@PostMapping("/user/notification/send/{userId}/{type}")
|
||||
Result<Void> sendNotification(@PathVariable("userId") Long userId,
|
||||
@PathVariable("type") Integer type,
|
||||
@RequestParam(value = "content", required = false) String content,
|
||||
@RequestParam(value = "relatedId", required = false) Long relatedId,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> sendNotification(Long userId, Integer type, String internalToken) {
|
||||
public Result<Void> sendNotification(Long userId, Integer type, String content, Long relatedId, String internalToken) {
|
||||
log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type);
|
||||
return Result.error("发送用户通知失败");
|
||||
}
|
||||
|
||||
+8
-2
@@ -7,6 +7,7 @@ import com.nuaa.video.comment.dto.CommentQueryDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.vo.CommentStatVO;
|
||||
import com.nuaa.video.comment.vo.CommentVO;
|
||||
import com.nuaa.video.comment.vo.AdminCommentVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,7 @@ public interface CommentService extends IService<Comment> {
|
||||
/**
|
||||
* 获取评论列表 - 管理接口
|
||||
*/
|
||||
Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword);
|
||||
Result<Page<AdminCommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword);
|
||||
|
||||
/**
|
||||
* 获取评论详情 - 管理接口
|
||||
@@ -111,8 +112,13 @@ public interface CommentService extends IService<Comment> {
|
||||
*/
|
||||
Result<Void> adminDeleteComment(Long commentId);
|
||||
|
||||
/**
|
||||
* 管理员恢复评论 - 管理接口
|
||||
*/
|
||||
Result<Void> adminRestoreComment(Long commentId);
|
||||
|
||||
/**
|
||||
* 获取评论统计信息 - 管理接口
|
||||
*/
|
||||
Result<CommentStatVO> getCommentStats();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -277,10 +277,10 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
if (!userId.equals(comment.getUserId())) {
|
||||
userFeignClient.sendNotification(comment.getUserId(), 2, internalServiceToken);
|
||||
userFeignClient.sendNotification(comment.getUserId(), 2, null, replyDTO.getVideoId(), internalServiceToken);
|
||||
}
|
||||
if (replyDTO.getReplyToId() != null && !userId.equals(replyDTO.getReplyToId())) {
|
||||
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, internalServiceToken);
|
||||
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, null, replyDTO.getVideoId(), internalServiceToken);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("发送回复通知失败", e);
|
||||
|
||||
+58
-8
@@ -17,6 +17,7 @@ import com.nuaa.video.comment.service.CommentService;
|
||||
import com.nuaa.video.comment.utils.SensitiveWordFilter;
|
||||
import com.nuaa.video.comment.vo.CommentStatVO;
|
||||
import com.nuaa.video.comment.vo.CommentVO;
|
||||
import com.nuaa.video.comment.vo.AdminCommentVO;
|
||||
import com.nuaa.video.comment.vo.ReplyVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -94,7 +95,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
|
||||
updateVideoCommentCountAsync(commentDTO.getVideoId(), 1);
|
||||
|
||||
sendCommentNotificationAsync(videoResult.getData(), userId);
|
||||
sendCommentNotificationAsync(videoResult.getData(), userId, commentDTO.getVideoId());
|
||||
|
||||
return Result.success(comment.getId());
|
||||
}
|
||||
@@ -361,7 +362,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword) {
|
||||
public Result<Page<AdminCommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword) {
|
||||
if (page == null || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
@@ -372,7 +373,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 管理员可以查看所有状态的评论,包括已删除的
|
||||
if (status != null) {
|
||||
queryWrapper.eq(Comment::getStatus, status);
|
||||
}
|
||||
|
||||
// 按视频ID筛选
|
||||
if (videoId != null) {
|
||||
@@ -397,12 +400,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
commentPage = page(commentPage, queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
Page<CommentVO> voPage = new Page<>();
|
||||
Page<AdminCommentVO> voPage = new Page<>();
|
||||
BeanUtils.copyProperties(commentPage, voPage, "records");
|
||||
|
||||
List<CommentVO> commentVOList = new ArrayList<>();
|
||||
List<AdminCommentVO> commentVOList = new ArrayList<>();
|
||||
for (Comment comment : commentPage.getRecords()) {
|
||||
CommentVO vo = convertToCommentVO(comment, null);
|
||||
AdminCommentVO vo = convertToAdminCommentVO(comment);
|
||||
commentVOList.add(vo);
|
||||
}
|
||||
|
||||
@@ -453,6 +456,23 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> adminRestoreComment(Long commentId) {
|
||||
Comment comment = getById(commentId);
|
||||
if (comment == null) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
if (comment.getStatus() == 1) {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
commentMapper.updateCommentStatus(commentId, 1, "admin");
|
||||
updateVideoCommentCountAsync(comment.getVideoId(), 1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<CommentStatVO> getCommentStats() {
|
||||
CommentStatVO statVO = new CommentStatVO();
|
||||
@@ -514,14 +534,44 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void sendCommentNotificationAsync(Object videoPayload, Long userId) {
|
||||
private AdminCommentVO convertToAdminCommentVO(Comment comment) {
|
||||
AdminCommentVO vo = new AdminCommentVO();
|
||||
BeanUtils.copyProperties(comment, vo);
|
||||
|
||||
try {
|
||||
Result<Object> userResult = userFeignClient.getUserProfile(comment.getUserId());
|
||||
if (userResult.getCode() == 200 && userResult.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
|
||||
vo.setUserNickname((String) userData.get("nickname"));
|
||||
vo.setUserAvatar((String) userData.get("avatar"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("管理端获取评论用户信息失败, userId={}", comment.getUserId(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
Result<Object> videoResult = contentFeignClient.getVideoInfo(comment.getVideoId());
|
||||
if (videoResult.getCode() == 200 && videoResult.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
|
||||
vo.setVideoTitle((String) videoData.get("title"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("管理端获取视频信息失败, videoId={}", comment.getVideoId(), e);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void sendCommentNotificationAsync(Object videoPayload, Long userId, Long videoId) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) videoPayload;
|
||||
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
|
||||
if (!userId.equals(videoAuthorId)) {
|
||||
userFeignClient.sendNotification(videoAuthorId, 2, internalServiceToken);
|
||||
userFeignClient.sendNotification(videoAuthorId, 2, null, videoId, internalServiceToken);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("发送评论通知失败", e);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.nuaa.video.comment.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理端评论VO
|
||||
*/
|
||||
@Data
|
||||
public class AdminCommentVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long videoId;
|
||||
|
||||
private String videoTitle;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String userNickname;
|
||||
|
||||
private String userAvatar;
|
||||
|
||||
private String content;
|
||||
|
||||
private Integer likeCount;
|
||||
|
||||
private Integer replyCount;
|
||||
|
||||
private Integer topStatus;
|
||||
|
||||
/**
|
||||
* 状态(0:已删除,1:正常)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.nuaa.video.comment.service;
|
||||
|
||||
import com.nuaa.video.comment.dto.ReplyAddDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.feign.UserFeignClient;
|
||||
import com.nuaa.video.comment.service.impl.CommentReplyServiceImpl;
|
||||
import com.nuaa.video.comment.service.impl.CommentServiceImpl;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("通知发送测试")
|
||||
class NotificationDispatchTest {
|
||||
|
||||
@Mock
|
||||
private UserFeignClient userFeignClient;
|
||||
|
||||
@InjectMocks
|
||||
private CommentServiceImpl commentService;
|
||||
|
||||
@InjectMocks
|
||||
private CommentReplyServiceImpl commentReplyService;
|
||||
|
||||
@Test
|
||||
@DisplayName("评论通知应携带视频ID作为 relatedId")
|
||||
void shouldSendCommentNotificationWithVideoId() {
|
||||
ReflectionTestUtils.setField(commentService, "internalServiceToken", "internal-token");
|
||||
|
||||
ReflectionTestUtils.invokeMethod(
|
||||
commentService,
|
||||
"sendCommentNotificationAsync",
|
||||
Map.of("userId", 200L),
|
||||
100L,
|
||||
300L
|
||||
);
|
||||
|
||||
verify(userFeignClient, timeout(1000)).sendNotification(200L, 2, null, 300L, "internal-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("回复通知应携带视频ID作为 relatedId")
|
||||
void shouldSendReplyNotificationWithVideoId() {
|
||||
ReflectionTestUtils.setField(commentReplyService, "internalServiceToken", "internal-token");
|
||||
|
||||
Comment comment = new Comment();
|
||||
comment.setUserId(200L);
|
||||
|
||||
ReplyAddDTO replyDTO = new ReplyAddDTO();
|
||||
replyDTO.setReplyToId(300L);
|
||||
replyDTO.setVideoId(400L);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(
|
||||
commentReplyService,
|
||||
"sendReplyNotificationAsync",
|
||||
comment,
|
||||
replyDTO,
|
||||
100L
|
||||
);
|
||||
|
||||
verify(userFeignClient, timeout(1000)).sendNotification(eq(200L), eq(2), isNull(), eq(400L), eq("internal-token"));
|
||||
verify(userFeignClient, timeout(1000)).sendNotification(eq(300L), eq(2), isNull(), eq(400L), eq("internal-token"));
|
||||
}
|
||||
}
|
||||
+31
-8
@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.content.dto.*;
|
||||
import com.nuaa.video.content.entity.ContentVideo;
|
||||
import com.nuaa.video.content.entity.ContentVideoPlayRecord;
|
||||
import com.nuaa.video.content.mapper.ContentVideoMapper;
|
||||
import com.nuaa.video.content.service.VideoService;
|
||||
import com.nuaa.video.content.vo.PlayHistoryVO;
|
||||
import com.nuaa.video.content.vo.UploadResultVO;
|
||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||
import com.nuaa.video.content.vo.VideoListVO;
|
||||
@@ -62,14 +64,7 @@ public class VideoController {
|
||||
@GetMapping("/public/{id}")
|
||||
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
Result<VideoInfoVO> result = videoService.getVideoInfo(id, userId);
|
||||
if (result.getData() == null) {
|
||||
return result;
|
||||
}
|
||||
if (result.getData().getPublishTime() == null) {
|
||||
return Result.error("视频暂未公开");
|
||||
}
|
||||
return result;
|
||||
return videoService.getVideoInfo(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,6 +218,34 @@ public class VideoController {
|
||||
return videoService.playVideo(recordDTO, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放历史
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public Result<Page<PlayHistoryVO>> getPlayHistory(
|
||||
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未获取到用户ID");
|
||||
}
|
||||
Page<ContentVideoPlayRecord> page = new Page<>(pageNum, pageSize);
|
||||
return videoService.getPlayHistory(page, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空播放历史
|
||||
*/
|
||||
@DeleteMapping("/history")
|
||||
public Result<Void> clearPlayHistory(HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未获取到用户ID");
|
||||
}
|
||||
return videoService.clearPlayHistory(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户视频列表
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端
|
||||
@@ -57,4 +58,30 @@ public interface UserFeignClient {
|
||||
Result<Void> increaseFavoriteCount(@PathVariable("userId") Long userId,
|
||||
@PathVariable("count") Integer count,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 增加用户硬币余额
|
||||
*/
|
||||
@PostMapping("/user/profile/count/coin/increase/{userId}/{count}")
|
||||
Result<Void> increaseCoinCount(@PathVariable("userId") Long userId,
|
||||
@PathVariable("count") Integer count,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 扣减用户硬币余额
|
||||
*/
|
||||
@PostMapping("/user/profile/count/coin/decrease/{userId}/{count}")
|
||||
Result<Void> decreaseCoinCount(@PathVariable("userId") Long userId,
|
||||
@PathVariable("count") Integer count,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 发送用户通知
|
||||
*/
|
||||
@PostMapping("/user/notification/send/{userId}/{type}")
|
||||
Result<Void> sendNotification(@PathVariable("userId") Long userId,
|
||||
@PathVariable("type") Integer type,
|
||||
@RequestParam(value = "content", required = false) String content,
|
||||
@RequestParam(value = "relatedId", required = false) Long relatedId,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
}
|
||||
|
||||
+18
@@ -39,6 +39,24 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
||||
log.error("增加用户收藏数量失败,用户ID:{}, 数量:{}", userId, count);
|
||||
return Result.error("更新用户收藏数量失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> increaseCoinCount(Long userId, Integer count, String internalToken) {
|
||||
log.error("增加用户硬币余额失败,用户ID:{}, 数量:{}", userId, count);
|
||||
return Result.error("更新用户硬币余额失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> decreaseCoinCount(Long userId, Integer count, String internalToken) {
|
||||
log.error("扣减用户硬币余额失败,用户ID:{}, 数量:{}", userId, count);
|
||||
return Result.error("扣减用户硬币余额失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> sendNotification(Long userId, Integer type, String content, Long relatedId, String internalToken) {
|
||||
log.error("发送用户通知失败,用户ID:{}, 类型:{}, relatedId:{}", userId, type, relatedId);
|
||||
return Result.error("发送用户通知失败");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.content.dto.*;
|
||||
import com.nuaa.video.content.entity.ContentVideo;
|
||||
import com.nuaa.video.content.entity.ContentVideoPlayRecord;
|
||||
import com.nuaa.video.content.vo.PlayHistoryVO;
|
||||
import com.nuaa.video.content.vo.UploadResultVO;
|
||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||
import com.nuaa.video.content.vo.VideoListVO;
|
||||
@@ -135,6 +137,23 @@ public interface VideoService extends IService<ContentVideo> {
|
||||
*/
|
||||
Result<Void> playVideo(PlayRecordDTO recordDTO, Long userId);
|
||||
|
||||
/**
|
||||
* 获取播放历史
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param userId 用户ID
|
||||
* @return 播放历史
|
||||
*/
|
||||
Result<Page<PlayHistoryVO>> getPlayHistory(Page<ContentVideoPlayRecord> page, Long userId);
|
||||
|
||||
/**
|
||||
* 清空播放历史
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 结果
|
||||
*/
|
||||
Result<Void> clearPlayHistory(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户视频列表
|
||||
*
|
||||
|
||||
+23
-1
@@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -52,6 +53,8 @@ public class FavoriteServiceImpl extends ServiceImpl<ContentUserFavoriteMapper,
|
||||
.map(favorite -> {
|
||||
FavoriteVO vo = new FavoriteVO();
|
||||
BeanUtils.copyProperties(favorite, vo);
|
||||
int visibleVideoCount = countVisibleFavoriteVideos(favorite.getId());
|
||||
vo.setVideoCount(visibleVideoCount);
|
||||
return vo;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@@ -192,4 +195,23 @@ public class FavoriteServiceImpl extends ServiceImpl<ContentUserFavoriteMapper,
|
||||
|
||||
return Result.success(resultPage);
|
||||
}
|
||||
}
|
||||
|
||||
private int countVisibleFavoriteVideos(Long favoriteId) {
|
||||
List<Long> videoIds = videoFavoriteMapper.selectList(
|
||||
new LambdaQueryWrapper<ContentVideoFavorite>()
|
||||
.eq(ContentVideoFavorite::getFavoriteId, favoriteId)
|
||||
).stream().map(ContentVideoFavorite::getVideoId).collect(Collectors.toList());
|
||||
|
||||
if (videoIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Long count = videoMapper.selectCount(
|
||||
new LambdaQueryWrapper<ContentVideo>()
|
||||
.in(ContentVideo::getId, videoIds)
|
||||
.eq(ContentVideo::getStatus, 1)
|
||||
);
|
||||
|
||||
return Math.toIntExact(Objects.requireNonNullElse(count, 0L));
|
||||
}
|
||||
}
|
||||
|
||||
+121
-18
@@ -17,6 +17,7 @@ import com.nuaa.video.content.service.TagService;
|
||||
import com.nuaa.video.content.service.VideoService;
|
||||
import com.nuaa.video.content.utils.VideoProcessor;
|
||||
import com.nuaa.video.content.vo.TagVO;
|
||||
import com.nuaa.video.content.vo.PlayHistoryVO;
|
||||
import com.nuaa.video.content.vo.UploadResultVO;
|
||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||
import com.nuaa.video.content.vo.VideoListVO;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -48,6 +50,8 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVideo> implements VideoService {
|
||||
|
||||
private static final ZoneId APP_ZONE_ID = ZoneId.of("Asia/Shanghai");
|
||||
|
||||
@Value("${service.auth.token}")
|
||||
private String internalServiceToken;
|
||||
|
||||
@@ -164,6 +168,12 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
|
||||
}
|
||||
|
||||
boolean isOwner = userId != null && userId.equals(video.getUserId());
|
||||
boolean isPublished = Objects.equals(video.getStatus(), VideoStatusEnum.PUBLISHED.getCode());
|
||||
if (!isPublished && !isOwner) {
|
||||
return Result.error(ResultCode.FORBIDDEN.getCode(), "视频暂未公开");
|
||||
}
|
||||
|
||||
// 转换为VO
|
||||
VideoInfoVO videoInfoVO = new VideoInfoVO();
|
||||
BeanUtils.copyProperties(video, videoInfoVO);
|
||||
@@ -205,6 +215,15 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
|
||||
int coinCount = videoCoinMapper.getUserCoinCount(userId, id);
|
||||
videoInfoVO.setIsCoined(coinCount > 0);
|
||||
|
||||
ContentVideoPlayRecord playRecord = getUserPlayRecord(userId, id);
|
||||
if (playRecord != null) {
|
||||
videoInfoVO.setPlayProgress(playRecord.getPlayTime());
|
||||
videoInfoVO.setIsPlayFinished(Objects.equals(playRecord.getIsFinished(), 1));
|
||||
} else {
|
||||
videoInfoVO.setPlayProgress(0);
|
||||
videoInfoVO.setIsPlayFinished(false);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(videoInfoVO);
|
||||
@@ -409,6 +428,7 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
}}
|
||||
);
|
||||
|
||||
sendVideoLikeNotificationAsync(video, userId);
|
||||
updateUserLikeCountAsync(video.getUserId(), 1);
|
||||
|
||||
return Result.success();
|
||||
@@ -473,25 +493,40 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
return Result.error("投币总数不能超过2个");
|
||||
}
|
||||
|
||||
// 添加投币记录
|
||||
ContentVideoCoin coin = new ContentVideoCoin();
|
||||
coin.setVideoId(videoId);
|
||||
coin.setUserId(userId);
|
||||
coin.setAmount(amount);
|
||||
coin.setCreateTime(LocalDateTime.now());
|
||||
videoCoinMapper.insert(coin);
|
||||
Result<Void> coinBalanceResult = userFeignClient.decreaseCoinCount(userId, amount, internalServiceToken);
|
||||
if (coinBalanceResult == null || coinBalanceResult.getCode() != 200) {
|
||||
String message = coinBalanceResult != null ? coinBalanceResult.getMessage() : "扣减硬币失败";
|
||||
return Result.error(message);
|
||||
}
|
||||
|
||||
// 发送统计消息,增加投币数
|
||||
videoMessageProducer.sendVideoStatsMessage(
|
||||
new VideoMessageProducer.VideoStatsMessage() {{
|
||||
setVideoId(videoId);
|
||||
setUserId(userId);
|
||||
setAction("coin");
|
||||
setCount(amount);
|
||||
}}
|
||||
);
|
||||
try {
|
||||
// 添加投币记录
|
||||
ContentVideoCoin coin = new ContentVideoCoin();
|
||||
coin.setVideoId(videoId);
|
||||
coin.setUserId(userId);
|
||||
coin.setAmount(amount);
|
||||
coin.setCreateTime(LocalDateTime.now());
|
||||
videoCoinMapper.insert(coin);
|
||||
|
||||
return Result.success();
|
||||
// 发送统计消息,增加投币数
|
||||
videoMessageProducer.sendVideoStatsMessage(
|
||||
new VideoMessageProducer.VideoStatsMessage() {{
|
||||
setVideoId(videoId);
|
||||
setUserId(userId);
|
||||
setAction("coin");
|
||||
setCount(amount);
|
||||
}}
|
||||
);
|
||||
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
Result<Void> rollbackResult = userFeignClient.increaseCoinCount(userId, amount, internalServiceToken);
|
||||
if (rollbackResult == null || rollbackResult.getCode() != 200) {
|
||||
log.error("投币失败且硬币余额补偿失败,videoId={}, userId={}, amount={}, rollbackResult={}",
|
||||
videoId, userId, amount, rollbackResult);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -673,6 +708,54 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<PlayHistoryVO>> getPlayHistory(Page<ContentVideoPlayRecord> page, Long userId) {
|
||||
Page<ContentVideoPlayRecord> recordPage = playRecordMapper.selectPage(
|
||||
page,
|
||||
new LambdaQueryWrapper<ContentVideoPlayRecord>()
|
||||
.eq(ContentVideoPlayRecord::getUserId, userId)
|
||||
.orderByDesc(ContentVideoPlayRecord::getUpdateTime)
|
||||
);
|
||||
|
||||
List<PlayHistoryVO> historyRecords = recordPage.getRecords().stream()
|
||||
.map(record -> {
|
||||
ContentVideo video = getById(record.getVideoId());
|
||||
if (video == null) {
|
||||
return null;
|
||||
}
|
||||
PlayHistoryVO vo = new PlayHistoryVO();
|
||||
vo.setVideoId(record.getVideoId());
|
||||
vo.setTitle(video.getTitle());
|
||||
vo.setCoverUrl(video.getCoverUrl());
|
||||
vo.setDuration(video.getDuration());
|
||||
vo.setPlayTime(record.getPlayTime());
|
||||
vo.setIsFinished(record.getIsFinished());
|
||||
vo.setUpdateTime(record.getUpdateTime());
|
||||
return vo;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Page<PlayHistoryVO> resultPage = new Page<>();
|
||||
resultPage.setRecords(historyRecords);
|
||||
resultPage.setTotal(recordPage.getTotal());
|
||||
resultPage.setSize(recordPage.getSize());
|
||||
resultPage.setCurrent(recordPage.getCurrent());
|
||||
resultPage.setPages(recordPage.getPages());
|
||||
|
||||
return Result.success(resultPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> clearPlayHistory(Long userId) {
|
||||
playRecordMapper.delete(
|
||||
new LambdaQueryWrapper<ContentVideoPlayRecord>()
|
||||
.eq(ContentVideoPlayRecord::getUserId, userId)
|
||||
);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<VideoListVO>> getUserVideos(Long userId, Page<ContentVideo> page, Long currentUserId) {
|
||||
VideoQueryDTO queryDTO = new VideoQueryDTO();
|
||||
@@ -885,7 +968,7 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
stats.put("totalVideos", totalVideos);
|
||||
|
||||
// 今日新增视频
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate today = LocalDate.now(APP_ZONE_ID);
|
||||
long todayNewVideos = count(new LambdaQueryWrapper<ContentVideo>()
|
||||
.ge(ContentVideo::getCreateTime, today.atStartOfDay()));
|
||||
stats.put("todayNewVideos", todayNewVideos);
|
||||
@@ -950,6 +1033,26 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
||||
});
|
||||
}
|
||||
|
||||
private ContentVideoPlayRecord getUserPlayRecord(Long userId, Long videoId) {
|
||||
return playRecordMapper.selectOne(
|
||||
new LambdaQueryWrapper<ContentVideoPlayRecord>()
|
||||
.eq(ContentVideoPlayRecord::getUserId, userId)
|
||||
.eq(ContentVideoPlayRecord::getVideoId, videoId)
|
||||
);
|
||||
}
|
||||
|
||||
private void sendVideoLikeNotificationAsync(ContentVideo video, Long operatorUserId) {
|
||||
runAfterCommitAsync("发送视频点赞通知", () -> {
|
||||
if (!operatorUserId.equals(video.getUserId())) {
|
||||
Result<Void> notifyResult = userFeignClient.sendNotification(video.getUserId(), 1, null, video.getId(), internalServiceToken);
|
||||
if (notifyResult == null || notifyResult.getCode() != 200) {
|
||||
log.warn("发送视频点赞通知失败,videoId={}, ownerUserId={}, operatorUserId={}, result={}",
|
||||
video.getId(), video.getUserId(), operatorUserId, notifyResult);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void runAfterCommitAsync(String action, Runnable task) {
|
||||
Runnable wrappedTask = () -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.nuaa.video.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 播放历史VO
|
||||
*/
|
||||
@Data
|
||||
public class PlayHistoryVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 视频标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 视频封面
|
||||
*/
|
||||
private String coverUrl;
|
||||
|
||||
/**
|
||||
* 视频时长
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 播放进度
|
||||
*/
|
||||
private Integer playTime;
|
||||
|
||||
/**
|
||||
* 是否已看完
|
||||
*/
|
||||
private Integer isFinished;
|
||||
|
||||
/**
|
||||
* 最后播放时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -68,6 +68,16 @@ public class VideoInfoVO implements Serializable {
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 视频格式
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* 分类ID
|
||||
*/
|
||||
@@ -123,6 +133,11 @@ public class VideoInfoVO implements Serializable {
|
||||
*/
|
||||
private Integer danmakuCount;
|
||||
|
||||
/**
|
||||
* 视频状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 是否已点赞
|
||||
*/
|
||||
@@ -138,11 +153,31 @@ public class VideoInfoVO implements Serializable {
|
||||
*/
|
||||
private Boolean isCoined;
|
||||
|
||||
/**
|
||||
* 播放进度(秒)
|
||||
*/
|
||||
private Integer playProgress;
|
||||
|
||||
/**
|
||||
* 是否已看完
|
||||
*/
|
||||
private Boolean isPlayFinished;
|
||||
|
||||
/**
|
||||
* 发布时间
|
||||
*/
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 审核员ID
|
||||
*/
|
||||
@@ -162,4 +197,4 @@ public class VideoInfoVO implements Serializable {
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime reviewTime;
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.nuaa.video.content.service;
|
||||
|
||||
import com.nuaa.video.content.entity.ContentVideo;
|
||||
import com.nuaa.video.content.feign.UserFeignClient;
|
||||
import com.nuaa.video.content.service.impl.VideoServiceImpl;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("视频通知发送测试")
|
||||
class NotificationDispatchTest {
|
||||
|
||||
@Mock
|
||||
private UserFeignClient userFeignClient;
|
||||
|
||||
@InjectMocks
|
||||
private VideoServiceImpl videoService;
|
||||
|
||||
@Test
|
||||
@DisplayName("视频点赞通知应携带视频ID作为 relatedId")
|
||||
void shouldSendVideoLikeNotificationWithVideoId() {
|
||||
ReflectionTestUtils.setField(videoService, "internalServiceToken", "internal-token");
|
||||
|
||||
ContentVideo video = new ContentVideo();
|
||||
video.setId(300L);
|
||||
video.setUserId(200L);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(videoService, "sendVideoLikeNotificationAsync", video, 100L);
|
||||
|
||||
verify(userFeignClient, timeout(1000)).sendNotification(200L, 1, null, 300L, "internal-token");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -33,12 +33,13 @@ public class AdminUserController {
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
Page<UserInfo> pageParam = new Page<>(page, size);
|
||||
return userInfoService.getAdminUserList(pageParam, keyword);
|
||||
return userInfoService.getAdminUserList(pageParam, keyword, status);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+28
@@ -172,6 +172,34 @@ public class UserProfileController {
|
||||
return userInfoService.increaseFavoriteCount(userId, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加用户硬币余额
|
||||
*/
|
||||
@PostMapping("/count/coin/increase/{userId}/{count}")
|
||||
public Result<Void> increaseCoinCount(
|
||||
@PathVariable Long userId,
|
||||
@PathVariable Integer count,
|
||||
HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
return userInfoService.increaseCoinCount(userId, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣减用户硬币余额
|
||||
*/
|
||||
@PostMapping("/count/coin/decrease/{userId}/{count}")
|
||||
public Result<Void> decreaseCoinCount(
|
||||
@PathVariable Long userId,
|
||||
@PathVariable Integer count,
|
||||
HttpServletRequest request) {
|
||||
if (!isInternalRequest(request)) {
|
||||
return Result.error("非法的内部调用");
|
||||
}
|
||||
return userInfoService.decreaseCoinCount(userId, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户名
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.nuaa.video.common.model.vo.Result;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证服务Feign客户端
|
||||
*/
|
||||
@@ -44,4 +46,11 @@ public interface AuthFeignClient {
|
||||
@PathVariable Long userId,
|
||||
@RequestParam String newPassword,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
|
||||
/**
|
||||
* 按账号状态查询用户ID - 管理接口
|
||||
*/
|
||||
@GetMapping("/admin/user-ids/status/{status}")
|
||||
Result<List<Long>> getUserIdsByStatus(@PathVariable Integer status,
|
||||
@RequestHeader("X-Internal-Token") String internalToken);
|
||||
}
|
||||
|
||||
@@ -43,4 +43,14 @@ public interface UserInfoMapper extends BaseMapper<UserInfo> {
|
||||
* 增加收藏数
|
||||
*/
|
||||
int increaseFavoriteCount(@Param("userId") Long userId, @Param("count") int count);
|
||||
|
||||
/**
|
||||
* 增加硬币余额
|
||||
*/
|
||||
int increaseCoinCount(@Param("userId") Long userId, @Param("count") int count);
|
||||
|
||||
/**
|
||||
* 扣减硬币余额
|
||||
*/
|
||||
int decreaseCoinCount(@Param("userId") Long userId, @Param("count") int count);
|
||||
}
|
||||
|
||||
@@ -93,10 +93,20 @@ public interface UserInfoService extends IService<UserInfo> {
|
||||
*/
|
||||
Result<Void> increaseFavoriteCount(Long userId, Integer count);
|
||||
|
||||
/**
|
||||
* 增加用户硬币余额
|
||||
*/
|
||||
Result<Void> increaseCoinCount(Long userId, Integer count);
|
||||
|
||||
/**
|
||||
* 扣减用户硬币余额
|
||||
*/
|
||||
Result<Void> decreaseCoinCount(Long userId, Integer count);
|
||||
|
||||
/**
|
||||
* 获取用户列表 - 管理接口
|
||||
*/
|
||||
Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword);
|
||||
Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword, Integer status);
|
||||
|
||||
/**
|
||||
* 获取用户详情 - 管理接口
|
||||
|
||||
+78
-7
@@ -27,6 +27,8 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -41,9 +43,15 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
|
||||
|
||||
private static final ZoneId APP_ZONE_ID = ZoneId.of("Asia/Shanghai");
|
||||
private static final long DEFAULT_AVATAR_MAX_SIZE_BYTES = 5L * 1024 * 1024;
|
||||
|
||||
@Value("${service.auth.token}")
|
||||
private String internalServiceToken;
|
||||
|
||||
@Value("${user.avatar.max-size-bytes:" + DEFAULT_AVATAR_MAX_SIZE_BYTES + "}")
|
||||
private long avatarMaxSizeBytes;
|
||||
|
||||
private final UserInfoMapper userInfoMapper;
|
||||
private final UserFollowMapper userFollowMapper;
|
||||
private final UserSettingService userSettingService;
|
||||
@@ -118,6 +126,19 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
||||
return Result.error(ResultCode.USER_NOT_FOUND.getCode(),ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
if (file == null || file.isEmpty()) {
|
||||
return Result.error("请选择头像文件");
|
||||
}
|
||||
|
||||
String contentType = file.getContentType();
|
||||
if (!StringUtils.hasText(contentType) || !contentType.startsWith("image/")) {
|
||||
return Result.error("仅支持上传图片格式头像");
|
||||
}
|
||||
|
||||
if (file.getSize() > avatarMaxSizeBytes) {
|
||||
return Result.error("头像文件不能超过5MB");
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
String avatarUrl = minioUtils.uploadImage(file);
|
||||
|
||||
@@ -127,8 +148,8 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
||||
userInfo.setUpdateTime(LocalDateTime.now());
|
||||
updateById(userInfo);
|
||||
|
||||
// 返回头像URL
|
||||
return Result.success(minioUtils.getPreSignedUrl(avatarUrl));
|
||||
// 返回对象路径,由前端统一拼接可访问的媒体地址
|
||||
return Result.success(avatarUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("上传头像失败", e);
|
||||
return Result.error("上传头像失败: " + e.getMessage());
|
||||
@@ -154,7 +175,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
||||
userInfo.setFollowingCount(0);
|
||||
userInfo.setVideoCount(0);
|
||||
userInfo.setLikeCount(0);
|
||||
userInfo.setCoinCount(0);
|
||||
userInfo.setCoinCount(20);
|
||||
userInfo.setFavoriteCount(0);
|
||||
userInfo.setViewCount(0L);
|
||||
userInfo.setLevel(1);
|
||||
@@ -247,14 +268,64 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword) {
|
||||
public Result<Void> increaseCoinCount(Long userId, Integer count) {
|
||||
if (count == null || count <= 0) {
|
||||
return Result.error("硬币数量无效");
|
||||
}
|
||||
|
||||
UserInfo userInfo = getById(userId);
|
||||
if (userInfo == null) {
|
||||
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
userInfoMapper.increaseCoinCount(userId, count);
|
||||
updateUserStatDailyAsync(userId, "coin", count);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> decreaseCoinCount(Long userId, Integer count) {
|
||||
if (count == null || count <= 0) {
|
||||
return Result.error("硬币数量无效");
|
||||
}
|
||||
|
||||
UserInfo userInfo = getById(userId);
|
||||
if (userInfo == null) {
|
||||
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), ResultCode.USER_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
int affectedRows = userInfoMapper.decreaseCoinCount(userId, count);
|
||||
if (affectedRows <= 0) {
|
||||
return Result.error("硬币余额不足");
|
||||
}
|
||||
|
||||
updateUserStatDailyAsync(userId, "coin", -count);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword, Integer status) {
|
||||
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 添加关键词搜索条件
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.like(UserInfo::getNickname, keyword)
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.like(UserInfo::getNickname, keyword)
|
||||
.or()
|
||||
.like(UserInfo::getSignature, keyword);
|
||||
.like(UserInfo::getSignature, keyword));
|
||||
}
|
||||
|
||||
if (status != null) {
|
||||
Result<List<Long>> userIdsResult = authFeignClient.getUserIdsByStatus(status, internalServiceToken);
|
||||
List<Long> userIds = userIdsResult.getData() == null ? Collections.emptyList() : userIdsResult.getData();
|
||||
if (userIds.isEmpty()) {
|
||||
Page<UserProfileVO> emptyPage = new Page<>(page.getCurrent(), page.getSize(), 0);
|
||||
emptyPage.setRecords(Collections.emptyList());
|
||||
return Result.success(emptyPage);
|
||||
}
|
||||
queryWrapper.in(UserInfo::getId, userIds);
|
||||
}
|
||||
|
||||
// 按创建时间降序排序
|
||||
@@ -346,7 +417,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
||||
stats.put("totalUsers", totalUsers);
|
||||
|
||||
// 今日新增用户
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate today = LocalDate.now(APP_ZONE_ID);
|
||||
long todayNewUsers = count(new LambdaQueryWrapper<UserInfo>()
|
||||
.ge(UserInfo::getCreateTime, today.atStartOfDay()));
|
||||
stats.put("todayNewUsers", todayNewUsers);
|
||||
|
||||
@@ -68,6 +68,11 @@ public class UserProfileVO implements Serializable {
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 硬币余额
|
||||
*/
|
||||
private Integer coinCount;
|
||||
|
||||
/**
|
||||
* 等级
|
||||
*/
|
||||
@@ -92,4 +97,4 @@ public class UserProfileVO implements Serializable {
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,19 @@
|
||||
WHERE id = #{userId}
|
||||
</update>
|
||||
|
||||
<update id="increaseCoinCount">
|
||||
UPDATE user_info
|
||||
SET coin_count = GREATEST(coin_count + #{count}, 0),
|
||||
update_time = NOW()
|
||||
WHERE id = #{userId}
|
||||
</update>
|
||||
|
||||
<update id="decreaseCoinCount">
|
||||
UPDATE user_info
|
||||
SET coin_count = coin_count - #{count},
|
||||
update_time = NOW()
|
||||
WHERE id = #{userId}
|
||||
AND coin_count >= #{count}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user