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")
|
@GetMapping("/list")
|
||||||
public Result<Page<AdminUserVO>> listUsers(
|
public Result<Page<AdminUserVO>> listUsers(
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@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}")
|
@GetMapping("/{id}")
|
||||||
public Result<AdminUserVO> getUser(@PathVariable Long id) {
|
public Result<AdminUserVO> getUser(@PathVariable Long id) {
|
||||||
return adminUserService.getUserDetail(id);
|
return adminUserService.getUserDetail(id);
|
||||||
@@ -52,7 +52,7 @@ public class AdminUserController {
|
|||||||
* 添加管理员
|
* 添加管理员
|
||||||
*/
|
*/
|
||||||
@OperationLog("添加管理员")
|
@OperationLog("添加管理员")
|
||||||
@PreAuthorize("hasAuthority('user:manage')")
|
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public Result<Void> addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
public Result<Void> addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
||||||
return adminUserService.addUser(adminUserDTO, getCurrentUsername());
|
return adminUserService.addUser(adminUserDTO, getCurrentUsername());
|
||||||
@@ -62,7 +62,7 @@ public class AdminUserController {
|
|||||||
* 更新管理员
|
* 更新管理员
|
||||||
*/
|
*/
|
||||||
@OperationLog("更新管理员")
|
@OperationLog("更新管理员")
|
||||||
@PreAuthorize("hasAuthority('user:manage')")
|
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||||
@PutMapping
|
@PutMapping
|
||||||
public Result<Void> updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
public Result<Void> updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
|
||||||
return adminUserService.updateUser(adminUserDTO, getCurrentUsername());
|
return adminUserService.updateUser(adminUserDTO, getCurrentUsername());
|
||||||
@@ -72,7 +72,7 @@ public class AdminUserController {
|
|||||||
* 删除管理员
|
* 删除管理员
|
||||||
*/
|
*/
|
||||||
@OperationLog("删除管理员")
|
@OperationLog("删除管理员")
|
||||||
@PreAuthorize("hasAuthority('user:manage')")
|
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public Result<Void> deleteUser(@PathVariable Long id) {
|
public Result<Void> deleteUser(@PathVariable Long id) {
|
||||||
return adminUserService.deleteUser(id);
|
return adminUserService.deleteUser(id);
|
||||||
@@ -82,7 +82,7 @@ public class AdminUserController {
|
|||||||
* 启用/禁用管理员
|
* 启用/禁用管理员
|
||||||
*/
|
*/
|
||||||
@OperationLog("修改管理员状态")
|
@OperationLog("修改管理员状态")
|
||||||
@PreAuthorize("hasAuthority('user:manage')")
|
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||||
@PutMapping("/{id}/status/{status}")
|
@PutMapping("/{id}/status/{status}")
|
||||||
public Result<Void> updateStatus(@PathVariable Long id, @PathVariable Integer status) {
|
public Result<Void> updateStatus(@PathVariable Long id, @PathVariable Integer status) {
|
||||||
return adminUserService.updateStatus(id, status, getCurrentUsername());
|
return adminUserService.updateStatus(id, status, getCurrentUsername());
|
||||||
@@ -92,7 +92,7 @@ public class AdminUserController {
|
|||||||
* 重置密码
|
* 重置密码
|
||||||
*/
|
*/
|
||||||
@OperationLog("重置管理员密码")
|
@OperationLog("重置管理员密码")
|
||||||
@PreAuthorize("hasAuthority('user:manage')")
|
@PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')")
|
||||||
@PutMapping("/{id}/reset-password")
|
@PutMapping("/{id}/reset-password")
|
||||||
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
|
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
|
||||||
return adminUserService.resetPassword(id, newPassword, getCurrentUsername());
|
return adminUserService.resetPassword(id, newPassword, getCurrentUsername());
|
||||||
@@ -108,4 +108,4 @@ public class AdminUserController {
|
|||||||
}
|
}
|
||||||
return "system";
|
return "system";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -34,8 +34,9 @@ public class CommentManageController {
|
|||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||||
@RequestParam(value = "userId", required = false) Long userId,
|
@RequestParam(value = "userId", required = false) Long userId,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
@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);
|
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(
|
public Result<Page<Object>> getUserList(
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
return userFeignClient.getUserList(page, size, keyword, internalServiceToken);
|
@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;
|
package com.nuaa.video.admin.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Pattern;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -29,7 +28,6 @@ public class AdminRoleDTO implements Serializable {
|
|||||||
* 角色编码
|
* 角色编码
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "角色编码不能为空")
|
@NotBlank(message = "角色编码不能为空")
|
||||||
@Pattern(regexp = "^ADMIN_[A-Z_]+$", message = "角色编码必须以ADMIN_开头")
|
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +36,7 @@ public class AdminRoleDTO implements Serializable {
|
|||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限ID集合
|
* 权限ID列表
|
||||||
*/
|
*/
|
||||||
private List<Long> permissionIds;
|
private List<Long> permissionIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public interface CommentFeignClient {
|
|||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||||
@RequestParam(value = "userId", required = false) Long userId,
|
@RequestParam(value = "userId", required = false) Long userId,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword,
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@RequestHeader("X-Internal-Token") String internalToken);
|
||||||
|
|
||||||
@@ -37,6 +38,13 @@ public interface CommentFeignClient {
|
|||||||
Result<Void> deleteComment(@PathVariable("commentId") Long commentId,
|
Result<Void> deleteComment(@PathVariable("commentId") Long commentId,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@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() {
|
return new CommentFeignClient() {
|
||||||
@Override
|
@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("获取评论列表失败:服务不可用");
|
return Result.error("获取评论列表失败:服务不可用");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,11 @@ public class CommentFeignClientFallbackFactory implements FallbackFactory<Commen
|
|||||||
return Result.error("删除评论失败:服务不可用");
|
return Result.error("删除评论失败:服务不可用");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> restoreComment(Long commentId, String internalToken) {
|
||||||
|
return Result.error("恢复评论失败:服务不可用");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Object> getCommentStats(String internalToken) {
|
public Result<Object> getCommentStats(String internalToken) {
|
||||||
return Result.error("获取评论统计信息失败:服务不可用");
|
return Result.error("获取评论统计信息失败:服务不可用");
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public interface UserFeignClient {
|
|||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword,
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@RequestHeader("X-Internal-Token") String internalToken);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
|||||||
log.error("用户服务调用失败:", cause);
|
log.error("用户服务调用失败:", cause);
|
||||||
return new UserFeignClient() {
|
return new UserFeignClient() {
|
||||||
@Override
|
@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("获取用户列表失败");
|
log.error("获取用户列表失败");
|
||||||
return Result.error("获取用户列表失败,用户服务不可用");
|
return Result.error("获取用户列表失败,用户服务不可用");
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-1
@@ -1,5 +1,9 @@
|
|||||||
package com.nuaa.video.admin.security;
|
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.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -14,7 +18,10 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +32,21 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
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 JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final AdminUserService adminUserService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
@@ -39,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// 从令牌中获取用户信息
|
// 从令牌中获取用户信息
|
||||||
Long adminId = jwtTokenProvider.getAdminIdFromToken(token);
|
Long adminId = jwtTokenProvider.getAdminIdFromToken(token);
|
||||||
String username = jwtTokenProvider.getUsernameFromToken(token);
|
String username = jwtTokenProvider.getUsernameFromToken(token);
|
||||||
List<String> authorities = jwtTokenProvider.getAuthoritiesFromToken(token);
|
List<String> authorities = resolveAuthorities(username, token);
|
||||||
|
|
||||||
// 转换为Spring Security的授权对象
|
// 转换为Spring Security的授权对象
|
||||||
List<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
|
List<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
|
||||||
@@ -72,6 +93,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
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令牌
|
* 从请求头中提取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
|
@Override
|
||||||
public Result<Page<AdminUserVO>> listUsers(Page<AdminUser> page, String keyword) {
|
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
|
// 转换为VO
|
||||||
Page<AdminUserVO> voPage = new Page<>();
|
Page<AdminUserVO> voPage = new Page<>();
|
||||||
BeanUtils.copyProperties(userPage, voPage, "records");
|
BeanUtils.copyProperties(userPage, voPage, "records");
|
||||||
|
|
||||||
List<AdminUserVO> userVOList = new ArrayList<>();
|
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();
|
AdminUserVO vo = new AdminUserVO();
|
||||||
BeanUtils.copyProperties(user, vo);
|
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.AdminPermission;
|
||||||
import com.nuaa.video.admin.entity.AdminRole;
|
import com.nuaa.video.admin.entity.AdminRole;
|
||||||
import com.nuaa.video.admin.entity.AdminUser;
|
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.AdminUserDetails;
|
||||||
import com.nuaa.video.admin.security.JwtTokenProvider;
|
import com.nuaa.video.admin.security.JwtTokenProvider;
|
||||||
import com.nuaa.video.admin.service.AdminUserService;
|
import com.nuaa.video.admin.service.AdminUserService;
|
||||||
@@ -44,6 +45,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
private final AuthenticationManager authenticationManager;
|
private final AuthenticationManager authenticationManager;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final AdminUserService adminUserService;
|
private final AdminUserService adminUserService;
|
||||||
|
private final AdminUserMapper adminUserMapper;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@Value("${login.retry-count:5}")
|
@Value("${login.retry-count:5}")
|
||||||
@@ -169,7 +171,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
@Override
|
@Override
|
||||||
public Result<LoginVO> getCurrentUser(AdminUserDetails adminUserDetails) {
|
public Result<LoginVO> getCurrentUser(AdminUserDetails adminUserDetails) {
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
AdminUser adminUser = adminUserService.getById(adminUserDetails.getId());
|
AdminUser adminUser = adminUserMapper.selectUserDetail(adminUserDetails.getId());
|
||||||
if (adminUser == null) {
|
if (adminUser == null) {
|
||||||
return Result.error("用户不存在");
|
return Result.error("用户不存在");
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -1,5 +1,6 @@
|
|||||||
package com.nuaa.video.auth.controller;
|
package com.nuaa.video.auth.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.nuaa.video.auth.entity.AuthUser;
|
import com.nuaa.video.auth.entity.AuthUser;
|
||||||
import com.nuaa.video.auth.service.AuthUserService;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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);
|
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) {
|
private boolean isInternalRequest(HttpServletRequest request) {
|
||||||
return internalServiceToken.equals(request.getHeader("X-Internal-Token"));
|
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.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.nuaa.video.comment.entity.Comment;
|
import com.nuaa.video.comment.entity.Comment;
|
||||||
import com.nuaa.video.comment.service.CommentService;
|
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.CommentStatVO;
|
||||||
import com.nuaa.video.comment.vo.CommentVO;
|
import com.nuaa.video.comment.vo.CommentVO;
|
||||||
import com.nuaa.video.common.model.vo.Result;
|
import com.nuaa.video.common.model.vo.Result;
|
||||||
@@ -30,18 +31,19 @@ public class AdminCommentController {
|
|||||||
* 获取评论列表 - 管理接口
|
* 获取评论列表 - 管理接口
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public Result<Page<CommentVO>> getCommentList(
|
public Result<Page<AdminCommentVO>> getCommentList(
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "videoId", required = false) Long videoId,
|
@RequestParam(value = "videoId", required = false) Long videoId,
|
||||||
@RequestParam(value = "userId", required = false) Long userId,
|
@RequestParam(value = "userId", required = false) Long userId,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword,
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
if (!isInternalRequest(request)) {
|
if (!isInternalRequest(request)) {
|
||||||
return Result.error("非法的内部调用");
|
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);
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户服务Feign客户端
|
* 用户服务Feign客户端
|
||||||
@@ -35,5 +36,7 @@ public interface UserFeignClient {
|
|||||||
@PostMapping("/user/notification/send/{userId}/{type}")
|
@PostMapping("/user/notification/send/{userId}/{type}")
|
||||||
Result<Void> sendNotification(@PathVariable("userId") Long userId,
|
Result<Void> sendNotification(@PathVariable("userId") Long userId,
|
||||||
@PathVariable("type") Integer type,
|
@PathVariable("type") Integer type,
|
||||||
|
@RequestParam(value = "content", required = false) String content,
|
||||||
|
@RequestParam(value = "relatedId", required = false) Long relatedId,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@RequestHeader("X-Internal-Token") String internalToken);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type);
|
||||||
return Result.error("发送用户通知失败");
|
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.entity.Comment;
|
||||||
import com.nuaa.video.comment.vo.CommentStatVO;
|
import com.nuaa.video.comment.vo.CommentStatVO;
|
||||||
import com.nuaa.video.comment.vo.CommentVO;
|
import com.nuaa.video.comment.vo.CommentVO;
|
||||||
|
import com.nuaa.video.comment.vo.AdminCommentVO;
|
||||||
import com.nuaa.video.common.model.vo.Result;
|
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> adminDeleteComment(Long commentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员恢复评论 - 管理接口
|
||||||
|
*/
|
||||||
|
Result<Void> adminRestoreComment(Long commentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取评论统计信息 - 管理接口
|
* 获取评论统计信息 - 管理接口
|
||||||
*/
|
*/
|
||||||
Result<CommentStatVO> getCommentStats();
|
Result<CommentStatVO> getCommentStats();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -277,10 +277,10 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
|
|||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
if (!userId.equals(comment.getUserId())) {
|
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())) {
|
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) {
|
} catch (Exception e) {
|
||||||
log.warn("发送回复通知失败", 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.utils.SensitiveWordFilter;
|
||||||
import com.nuaa.video.comment.vo.CommentStatVO;
|
import com.nuaa.video.comment.vo.CommentStatVO;
|
||||||
import com.nuaa.video.comment.vo.CommentVO;
|
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.comment.vo.ReplyVO;
|
||||||
import com.nuaa.video.common.model.vo.Result;
|
import com.nuaa.video.common.model.vo.Result;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -94,7 +95,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
|
|
||||||
updateVideoCommentCountAsync(commentDTO.getVideoId(), 1);
|
updateVideoCommentCountAsync(commentDTO.getVideoId(), 1);
|
||||||
|
|
||||||
sendCommentNotificationAsync(videoResult.getData(), userId);
|
sendCommentNotificationAsync(videoResult.getData(), userId, commentDTO.getVideoId());
|
||||||
|
|
||||||
return Result.success(comment.getId());
|
return Result.success(comment.getId());
|
||||||
}
|
}
|
||||||
@@ -361,7 +362,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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) {
|
if (page == null || page < 1) {
|
||||||
page = 1;
|
page = 1;
|
||||||
}
|
}
|
||||||
@@ -372,7 +373,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
// 管理员可以查看所有状态的评论,包括已删除的
|
if (status != null) {
|
||||||
|
queryWrapper.eq(Comment::getStatus, status);
|
||||||
|
}
|
||||||
|
|
||||||
// 按视频ID筛选
|
// 按视频ID筛选
|
||||||
if (videoId != null) {
|
if (videoId != null) {
|
||||||
@@ -397,12 +400,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
commentPage = page(commentPage, queryWrapper);
|
commentPage = page(commentPage, queryWrapper);
|
||||||
|
|
||||||
// 转换为VO
|
// 转换为VO
|
||||||
Page<CommentVO> voPage = new Page<>();
|
Page<AdminCommentVO> voPage = new Page<>();
|
||||||
BeanUtils.copyProperties(commentPage, voPage, "records");
|
BeanUtils.copyProperties(commentPage, voPage, "records");
|
||||||
|
|
||||||
List<CommentVO> commentVOList = new ArrayList<>();
|
List<AdminCommentVO> commentVOList = new ArrayList<>();
|
||||||
for (Comment comment : commentPage.getRecords()) {
|
for (Comment comment : commentPage.getRecords()) {
|
||||||
CommentVO vo = convertToCommentVO(comment, null);
|
AdminCommentVO vo = convertToAdminCommentVO(comment);
|
||||||
commentVOList.add(vo);
|
commentVOList.add(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,6 +456,23 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
return Result.success();
|
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
|
@Override
|
||||||
public Result<CommentStatVO> getCommentStats() {
|
public Result<CommentStatVO> getCommentStats() {
|
||||||
CommentStatVO statVO = new CommentStatVO();
|
CommentStatVO statVO = new CommentStatVO();
|
||||||
@@ -514,14 +534,44 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
|
|||||||
return vo;
|
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(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> videoData = (Map<String, Object>) videoPayload;
|
Map<String, Object> videoData = (Map<String, Object>) videoPayload;
|
||||||
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
|
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
|
||||||
if (!userId.equals(videoAuthorId)) {
|
if (!userId.equals(videoAuthorId)) {
|
||||||
userFeignClient.sendNotification(videoAuthorId, 2, internalServiceToken);
|
userFeignClient.sendNotification(videoAuthorId, 2, null, videoId, internalServiceToken);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("发送评论通知失败", 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.common.model.vo.Result;
|
||||||
import com.nuaa.video.content.dto.*;
|
import com.nuaa.video.content.dto.*;
|
||||||
import com.nuaa.video.content.entity.ContentVideo;
|
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.mapper.ContentVideoMapper;
|
||||||
import com.nuaa.video.content.service.VideoService;
|
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.UploadResultVO;
|
||||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||||
import com.nuaa.video.content.vo.VideoListVO;
|
import com.nuaa.video.content.vo.VideoListVO;
|
||||||
@@ -62,14 +64,7 @@ public class VideoController {
|
|||||||
@GetMapping("/public/{id}")
|
@GetMapping("/public/{id}")
|
||||||
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
|
public Result<VideoInfoVO> getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) {
|
||||||
Long userId = getUserIdFromRequest(request);
|
Long userId = getUserIdFromRequest(request);
|
||||||
Result<VideoInfoVO> result = videoService.getVideoInfo(id, userId);
|
return videoService.getVideoInfo(id, userId);
|
||||||
if (result.getData() == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (result.getData().getPublishTime() == null) {
|
|
||||||
return Result.error("视频暂未公开");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,6 +218,34 @@ public class VideoController {
|
|||||||
return videoService.playVideo(recordDTO, userId);
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户服务Feign客户端
|
* 用户服务Feign客户端
|
||||||
@@ -57,4 +58,30 @@ public interface UserFeignClient {
|
|||||||
Result<Void> increaseFavoriteCount(@PathVariable("userId") Long userId,
|
Result<Void> increaseFavoriteCount(@PathVariable("userId") Long userId,
|
||||||
@PathVariable("count") Integer count,
|
@PathVariable("count") Integer count,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@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);
|
log.error("增加用户收藏数量失败,用户ID:{}, 数量:{}", userId, count);
|
||||||
return Result.error("更新用户收藏数量失败");
|
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.common.model.vo.Result;
|
||||||
import com.nuaa.video.content.dto.*;
|
import com.nuaa.video.content.dto.*;
|
||||||
import com.nuaa.video.content.entity.ContentVideo;
|
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.UploadResultVO;
|
||||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||||
import com.nuaa.video.content.vo.VideoListVO;
|
import com.nuaa.video.content.vo.VideoListVO;
|
||||||
@@ -135,6 +137,23 @@ public interface VideoService extends IService<ContentVideo> {
|
|||||||
*/
|
*/
|
||||||
Result<Void> playVideo(PlayRecordDTO recordDTO, Long userId);
|
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.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +53,8 @@ public class FavoriteServiceImpl extends ServiceImpl<ContentUserFavoriteMapper,
|
|||||||
.map(favorite -> {
|
.map(favorite -> {
|
||||||
FavoriteVO vo = new FavoriteVO();
|
FavoriteVO vo = new FavoriteVO();
|
||||||
BeanUtils.copyProperties(favorite, vo);
|
BeanUtils.copyProperties(favorite, vo);
|
||||||
|
int visibleVideoCount = countVisibleFavoriteVideos(favorite.getId());
|
||||||
|
vo.setVideoCount(visibleVideoCount);
|
||||||
return vo;
|
return vo;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -192,4 +195,23 @@ public class FavoriteServiceImpl extends ServiceImpl<ContentUserFavoriteMapper,
|
|||||||
|
|
||||||
return Result.success(resultPage);
|
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.service.VideoService;
|
||||||
import com.nuaa.video.content.utils.VideoProcessor;
|
import com.nuaa.video.content.utils.VideoProcessor;
|
||||||
import com.nuaa.video.content.vo.TagVO;
|
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.UploadResultVO;
|
||||||
import com.nuaa.video.content.vo.VideoInfoVO;
|
import com.nuaa.video.content.vo.VideoInfoVO;
|
||||||
import com.nuaa.video.content.vo.VideoListVO;
|
import com.nuaa.video.content.vo.VideoListVO;
|
||||||
@@ -32,6 +33,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -48,6 +50,8 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVideo> implements VideoService {
|
public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVideo> implements VideoService {
|
||||||
|
|
||||||
|
private static final ZoneId APP_ZONE_ID = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
@Value("${service.auth.token}")
|
@Value("${service.auth.token}")
|
||||||
private String internalServiceToken;
|
private String internalServiceToken;
|
||||||
|
|
||||||
@@ -164,6 +168,12 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
return Result.error(ResultCode.NOT_FOUND.getCode(), "视频不存在");
|
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
|
// 转换为VO
|
||||||
VideoInfoVO videoInfoVO = new VideoInfoVO();
|
VideoInfoVO videoInfoVO = new VideoInfoVO();
|
||||||
BeanUtils.copyProperties(video, videoInfoVO);
|
BeanUtils.copyProperties(video, videoInfoVO);
|
||||||
@@ -205,6 +215,15 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
|
|
||||||
int coinCount = videoCoinMapper.getUserCoinCount(userId, id);
|
int coinCount = videoCoinMapper.getUserCoinCount(userId, id);
|
||||||
videoInfoVO.setIsCoined(coinCount > 0);
|
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);
|
return Result.success(videoInfoVO);
|
||||||
@@ -409,6 +428,7 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
}}
|
}}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
sendVideoLikeNotificationAsync(video, userId);
|
||||||
updateUserLikeCountAsync(video.getUserId(), 1);
|
updateUserLikeCountAsync(video.getUserId(), 1);
|
||||||
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
@@ -473,25 +493,40 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
return Result.error("投币总数不能超过2个");
|
return Result.error("投币总数不能超过2个");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加投币记录
|
Result<Void> coinBalanceResult = userFeignClient.decreaseCoinCount(userId, amount, internalServiceToken);
|
||||||
ContentVideoCoin coin = new ContentVideoCoin();
|
if (coinBalanceResult == null || coinBalanceResult.getCode() != 200) {
|
||||||
coin.setVideoId(videoId);
|
String message = coinBalanceResult != null ? coinBalanceResult.getMessage() : "扣减硬币失败";
|
||||||
coin.setUserId(userId);
|
return Result.error(message);
|
||||||
coin.setAmount(amount);
|
}
|
||||||
coin.setCreateTime(LocalDateTime.now());
|
|
||||||
videoCoinMapper.insert(coin);
|
|
||||||
|
|
||||||
// 发送统计消息,增加投币数
|
try {
|
||||||
videoMessageProducer.sendVideoStatsMessage(
|
// 添加投币记录
|
||||||
new VideoMessageProducer.VideoStatsMessage() {{
|
ContentVideoCoin coin = new ContentVideoCoin();
|
||||||
setVideoId(videoId);
|
coin.setVideoId(videoId);
|
||||||
setUserId(userId);
|
coin.setUserId(userId);
|
||||||
setAction("coin");
|
coin.setAmount(amount);
|
||||||
setCount(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
|
@Override
|
||||||
@@ -673,6 +708,54 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
return Result.success();
|
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
|
@Override
|
||||||
public Result<Page<VideoListVO>> getUserVideos(Long userId, Page<ContentVideo> page, Long currentUserId) {
|
public Result<Page<VideoListVO>> getUserVideos(Long userId, Page<ContentVideo> page, Long currentUserId) {
|
||||||
VideoQueryDTO queryDTO = new VideoQueryDTO();
|
VideoQueryDTO queryDTO = new VideoQueryDTO();
|
||||||
@@ -885,7 +968,7 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
|
|||||||
stats.put("totalVideos", totalVideos);
|
stats.put("totalVideos", totalVideos);
|
||||||
|
|
||||||
// 今日新增视频
|
// 今日新增视频
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now(APP_ZONE_ID);
|
||||||
long todayNewVideos = count(new LambdaQueryWrapper<ContentVideo>()
|
long todayNewVideos = count(new LambdaQueryWrapper<ContentVideo>()
|
||||||
.ge(ContentVideo::getCreateTime, today.atStartOfDay()));
|
.ge(ContentVideo::getCreateTime, today.atStartOfDay()));
|
||||||
stats.put("todayNewVideos", todayNewVideos);
|
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) {
|
private void runAfterCommitAsync(String action, Runnable task) {
|
||||||
Runnable wrappedTask = () -> CompletableFuture.runAsync(() -> {
|
Runnable wrappedTask = () -> CompletableFuture.runAsync(() -> {
|
||||||
try {
|
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 Integer height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频格式
|
||||||
|
*/
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件大小
|
||||||
|
*/
|
||||||
|
private Long size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分类ID
|
* 分类ID
|
||||||
*/
|
*/
|
||||||
@@ -123,6 +133,11 @@ public class VideoInfoVO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private Integer danmakuCount;
|
private Integer danmakuCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频状态
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否已点赞
|
* 是否已点赞
|
||||||
*/
|
*/
|
||||||
@@ -138,11 +153,31 @@ public class VideoInfoVO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private Boolean isCoined;
|
private Boolean isCoined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放进度(秒)
|
||||||
|
*/
|
||||||
|
private Integer playProgress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已看完
|
||||||
|
*/
|
||||||
|
private Boolean isPlayFinished;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布时间
|
* 发布时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime publishTime;
|
private LocalDateTime publishTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核员ID
|
* 审核员ID
|
||||||
*/
|
*/
|
||||||
@@ -162,4 +197,4 @@ public class VideoInfoVO implements Serializable {
|
|||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime reviewTime;
|
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 = "page", defaultValue = "1") Integer page,
|
||||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword,
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
if (!isInternalRequest(request)) {
|
if (!isInternalRequest(request)) {
|
||||||
return Result.error("非法的内部调用");
|
return Result.error("非法的内部调用");
|
||||||
}
|
}
|
||||||
Page<UserInfo> pageParam = new Page<>(page, size);
|
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);
|
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.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证服务Feign客户端
|
* 认证服务Feign客户端
|
||||||
*/
|
*/
|
||||||
@@ -44,4 +46,11 @@ public interface AuthFeignClient {
|
|||||||
@PathVariable Long userId,
|
@PathVariable Long userId,
|
||||||
@RequestParam String newPassword,
|
@RequestParam String newPassword,
|
||||||
@RequestHeader("X-Internal-Token") String internalToken);
|
@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 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> 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.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -41,9 +43,15 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
|
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}")
|
@Value("${service.auth.token}")
|
||||||
private String internalServiceToken;
|
private String internalServiceToken;
|
||||||
|
|
||||||
|
@Value("${user.avatar.max-size-bytes:" + DEFAULT_AVATAR_MAX_SIZE_BYTES + "}")
|
||||||
|
private long avatarMaxSizeBytes;
|
||||||
|
|
||||||
private final UserInfoMapper userInfoMapper;
|
private final UserInfoMapper userInfoMapper;
|
||||||
private final UserFollowMapper userFollowMapper;
|
private final UserFollowMapper userFollowMapper;
|
||||||
private final UserSettingService userSettingService;
|
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());
|
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);
|
String avatarUrl = minioUtils.uploadImage(file);
|
||||||
|
|
||||||
@@ -127,8 +148,8 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
|||||||
userInfo.setUpdateTime(LocalDateTime.now());
|
userInfo.setUpdateTime(LocalDateTime.now());
|
||||||
updateById(userInfo);
|
updateById(userInfo);
|
||||||
|
|
||||||
// 返回头像URL
|
// 返回对象路径,由前端统一拼接可访问的媒体地址
|
||||||
return Result.success(minioUtils.getPreSignedUrl(avatarUrl));
|
return Result.success(avatarUrl);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("上传头像失败", e);
|
log.error("上传头像失败", e);
|
||||||
return Result.error("上传头像失败: " + e.getMessage());
|
return Result.error("上传头像失败: " + e.getMessage());
|
||||||
@@ -154,7 +175,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
|||||||
userInfo.setFollowingCount(0);
|
userInfo.setFollowingCount(0);
|
||||||
userInfo.setVideoCount(0);
|
userInfo.setVideoCount(0);
|
||||||
userInfo.setLikeCount(0);
|
userInfo.setLikeCount(0);
|
||||||
userInfo.setCoinCount(0);
|
userInfo.setCoinCount(20);
|
||||||
userInfo.setFavoriteCount(0);
|
userInfo.setFavoriteCount(0);
|
||||||
userInfo.setViewCount(0L);
|
userInfo.setViewCount(0L);
|
||||||
userInfo.setLevel(1);
|
userInfo.setLevel(1);
|
||||||
@@ -247,14 +268,64 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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<>();
|
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
// 添加关键词搜索条件
|
// 添加关键词搜索条件
|
||||||
if (StringUtils.hasText(keyword)) {
|
if (StringUtils.hasText(keyword)) {
|
||||||
queryWrapper.like(UserInfo::getNickname, keyword)
|
queryWrapper.and(wrapper -> wrapper
|
||||||
|
.like(UserInfo::getNickname, keyword)
|
||||||
.or()
|
.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);
|
stats.put("totalUsers", totalUsers);
|
||||||
|
|
||||||
// 今日新增用户
|
// 今日新增用户
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now(APP_ZONE_ID);
|
||||||
long todayNewUsers = count(new LambdaQueryWrapper<UserInfo>()
|
long todayNewUsers = count(new LambdaQueryWrapper<UserInfo>()
|
||||||
.ge(UserInfo::getCreateTime, today.atStartOfDay()));
|
.ge(UserInfo::getCreateTime, today.atStartOfDay()));
|
||||||
stats.put("todayNewUsers", todayNewUsers);
|
stats.put("todayNewUsers", todayNewUsers);
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public class UserProfileVO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private Integer likeCount;
|
private Integer likeCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬币余额
|
||||||
|
*/
|
||||||
|
private Integer coinCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 等级
|
* 等级
|
||||||
*/
|
*/
|
||||||
@@ -92,4 +97,4 @@ public class UserProfileVO implements Serializable {
|
|||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,19 @@
|
|||||||
WHERE id = #{userId}
|
WHERE id = #{userId}
|
||||||
</update>
|
</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>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user