diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminRoleController.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminRoleController.java new file mode 100644 index 0000000..2b95cbf --- /dev/null +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminRoleController.java @@ -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> 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> listAllRoles() { + return adminRoleService.listAllRoles(); + } + + @PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')") + @GetMapping("/{id}") + public Result getRoleDetail(@PathVariable Long id) { + return adminRoleService.getRoleDetail(id); + } + + @OperationLog("新增角色") + @PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')") + @PostMapping + public Result addRole(@RequestBody @Valid AdminRoleDTO roleDTO) { + return adminRoleService.addRole(roleDTO, getCurrentUsername()); + } + + @OperationLog("更新角色") + @PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')") + @PutMapping + public Result updateRole(@RequestBody @Valid AdminRoleDTO roleDTO) { + return adminRoleService.updateRole(roleDTO, getCurrentUsername()); + } + + @OperationLog("角色授权") + @PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')") + @PutMapping("/{id}/permissions") + public Result updateRolePermissions(@PathVariable Long id, @RequestBody List permissionIds) { + return adminRoleService.updateRolePermissions(id, permissionIds, getCurrentUsername()); + } + + @OperationLog("删除角色") + @PreAuthorize("hasAuthority('role:manage') or hasRole('ADMIN_SUPER')") + @DeleteMapping("/{id}") + public Result 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> listPermissions() { + return adminPermissionService.listPermissions(); + } + + private String getCurrentUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + return authentication.getName(); + } + return "system"; + } +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java index 7b35fe3..dab8149 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java @@ -29,7 +29,7 @@ public class AdminUserController { /** * 获取管理员列表 */ - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @GetMapping("/list") public Result> listUsers( @RequestParam(value = "page", defaultValue = "1") Integer page, @@ -42,7 +42,7 @@ public class AdminUserController { /** * 获取管理员详情 */ - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @GetMapping("/{id}") public Result getUser(@PathVariable Long id) { return adminUserService.getUserDetail(id); @@ -52,7 +52,7 @@ public class AdminUserController { * 添加管理员 */ @OperationLog("添加管理员") - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @PostMapping public Result addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) { return adminUserService.addUser(adminUserDTO, getCurrentUsername()); @@ -62,7 +62,7 @@ public class AdminUserController { * 更新管理员 */ @OperationLog("更新管理员") - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @PutMapping public Result updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) { return adminUserService.updateUser(adminUserDTO, getCurrentUsername()); @@ -72,7 +72,7 @@ public class AdminUserController { * 删除管理员 */ @OperationLog("删除管理员") - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @DeleteMapping("/{id}") public Result deleteUser(@PathVariable Long id) { return adminUserService.deleteUser(id); @@ -82,7 +82,7 @@ public class AdminUserController { * 启用/禁用管理员 */ @OperationLog("修改管理员状态") - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @PutMapping("/{id}/status/{status}") public Result updateStatus(@PathVariable Long id, @PathVariable Integer status) { return adminUserService.updateStatus(id, status, getCurrentUsername()); @@ -92,7 +92,7 @@ public class AdminUserController { * 重置密码 */ @OperationLog("重置管理员密码") - @PreAuthorize("hasAuthority('user:manage')") + @PreAuthorize("hasAuthority('user:manage') or hasRole('ADMIN_SUPER')") @PutMapping("/{id}/reset-password") public Result resetPassword(@PathVariable Long id, @RequestParam String newPassword) { return adminUserService.resetPassword(id, newPassword, getCurrentUsername()); @@ -108,4 +108,4 @@ public class AdminUserController { } return "system"; } -} \ No newline at end of file +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/CommentManageController.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/CommentManageController.java index 9af798d..1d1e298 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/CommentManageController.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/CommentManageController.java @@ -34,8 +34,9 @@ public class CommentManageController { @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "videoId", required = false) Long videoId, @RequestParam(value = "userId", required = false) Long userId, + @RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "keyword", required = false) String keyword) { - return commentFeignClient.getCommentList(page, size, videoId, userId, keyword, internalServiceToken); + return commentFeignClient.getCommentList(page, size, videoId, userId, status, keyword, internalServiceToken); } /** @@ -57,6 +58,16 @@ public class CommentManageController { return commentFeignClient.deleteComment(commentId, internalServiceToken); } + /** + * 恢复评论 + */ + @OperationLog("恢复评论") + @PreAuthorize("hasAuthority('comment:manage')") + @PutMapping("/{commentId}/restore") + public Result restoreComment(@PathVariable Long commentId) { + return commentFeignClient.restoreComment(commentId, internalServiceToken); + } + /** * 获取评论统计信息 */ diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java index cda5491..76e427f 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java @@ -32,8 +32,9 @@ public class UserManageController { public Result> getUserList( @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, - @RequestParam(value = "keyword", required = false) String keyword) { - return userFeignClient.getUserList(page, size, keyword, internalServiceToken); + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "status", required = false) Integer status) { + return userFeignClient.getUserList(page, size, keyword, status, internalServiceToken); } /** diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java index 4787379..38c60b8 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java @@ -1,7 +1,6 @@ package com.nuaa.video.admin.dto; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; import lombok.Data; import java.io.Serializable; @@ -29,7 +28,6 @@ public class AdminRoleDTO implements Serializable { * 角色编码 */ @NotBlank(message = "角色编码不能为空") - @Pattern(regexp = "^ADMIN_[A-Z_]+$", message = "角色编码必须以ADMIN_开头") private String code; /** @@ -38,7 +36,7 @@ public class AdminRoleDTO implements Serializable { private String description; /** - * 权限ID集合 + * 权限ID列表 */ private List permissionIds; -} \ No newline at end of file +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java index 112edb4..b68fcc7 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java @@ -20,6 +20,7 @@ public interface CommentFeignClient { @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "videoId", required = false) Long videoId, @RequestParam(value = "userId", required = false) Long userId, + @RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "keyword", required = false) String keyword, @RequestHeader("X-Internal-Token") String internalToken); @@ -37,6 +38,13 @@ public interface CommentFeignClient { Result deleteComment(@PathVariable("commentId") Long commentId, @RequestHeader("X-Internal-Token") String internalToken); + /** + * 恢复评论 + */ + @PutMapping("/comment/admin/{commentId}/restore") + Result restoreComment(@PathVariable("commentId") Long commentId, + @RequestHeader("X-Internal-Token") String internalToken); + /** * 获取评论统计信息 */ diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java index b5a1079..86dc658 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java @@ -19,7 +19,7 @@ public class CommentFeignClientFallbackFactory implements FallbackFactory> getCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword, String internalToken) { + public Result> getCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword, String internalToken) { return Result.error("获取评论列表失败:服务不可用"); } @@ -33,6 +33,11 @@ public class CommentFeignClientFallbackFactory implements FallbackFactory restoreComment(Long commentId, String internalToken) { + return Result.error("恢复评论失败:服务不可用"); + } + @Override public Result getCommentStats(String internalToken) { return Result.error("获取评论统计信息失败:服务不可用"); diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java index 1892488..0bfaf79 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java @@ -19,6 +19,7 @@ public interface UserFeignClient { @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "status", required = false) Integer status, @RequestHeader("X-Internal-Token") String internalToken); /** diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java index 1e3966a..57550d1 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java @@ -18,7 +18,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory> getUserList(Integer page, Integer size, String keyword, String internalToken) { + public Result> getUserList(Integer page, Integer size, String keyword, Integer status, String internalToken) { log.error("获取用户列表失败"); return Result.error("获取用户列表失败,用户服务不可用"); } diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java index c1a7d16..c308eae 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java @@ -1,5 +1,9 @@ package com.nuaa.video.admin.security; +import com.nuaa.video.admin.entity.AdminPermission; +import com.nuaa.video.admin.entity.AdminRole; +import com.nuaa.video.admin.entity.AdminUser; +import com.nuaa.video.admin.service.AdminUserService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -14,7 +18,10 @@ import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** @@ -25,7 +32,21 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final List SUPER_ADMIN_AUTHORITIES = List.of( + "user:manage", + "role:manage", + "permission:manage", + "video:manage", + "comment:manage", + "tag:manage", + "category:manage", + "recommend:manage", + "system:config", + "operation:log" + ); + private final JwtTokenProvider jwtTokenProvider; + private final AdminUserService adminUserService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -39,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 从令牌中获取用户信息 Long adminId = jwtTokenProvider.getAdminIdFromToken(token); String username = jwtTokenProvider.getUsernameFromToken(token); - List authorities = jwtTokenProvider.getAuthoritiesFromToken(token); + List authorities = resolveAuthorities(username, token); // 转换为Spring Security的授权对象 List simpleGrantedAuthorities = authorities.stream() @@ -72,6 +93,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + private List resolveAuthorities(String username, String token) { + AdminUser adminUser = adminUserService.getByUsername(username); + if (adminUser != null) { + Set 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 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令牌 */ diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminPermissionService.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminPermissionService.java new file mode 100644 index 0000000..085b22d --- /dev/null +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminPermissionService.java @@ -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> listPermissions(); +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminRoleService.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminRoleService.java new file mode 100644 index 0000000..beb246b --- /dev/null +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/AdminRoleService.java @@ -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> listRoles(Page page, String keyword); + + Result> listAllRoles(); + + Result getRoleDetail(Long id); + + Result addRole(AdminRoleDTO roleDTO, String operator); + + Result updateRole(AdminRoleDTO roleDTO, String operator); + + Result updateRolePermissions(Long roleId, List permissionIds, String operator); + + Result deleteRole(Long id); +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminPermissionServiceImpl.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminPermissionServiceImpl.java new file mode 100644 index 0000000..ffbfa13 --- /dev/null +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminPermissionServiceImpl.java @@ -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> listPermissions() { + List permissions = adminPermissionMapper.selectList(new LambdaQueryWrapper() + .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; + } +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminRoleServiceImpl.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminRoleServiceImpl.java new file mode 100644 index 0000000..0cf5ab1 --- /dev/null +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminRoleServiceImpl.java @@ -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> listRoles(Page page, String keyword) { + Page rolePage = adminRoleMapper.selectRoleList(new Page<>(page.getCurrent(), page.getSize()), keyword); + Page 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> listAllRoles() { + List roles = adminRoleMapper.selectRoleList(new Page<>(1, 200), null).getRecords(); + return Result.success(roles.stream().map(this::toRoleVO).collect(Collectors.toList())); + } + + @Override + public Result 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 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 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 updateRolePermissions(Long roleId, List 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 deleteRole(Long id) { + AdminRole existing = adminRoleMapper.selectById(id); + if (existing == null) { + return Result.error("角色不存在"); + } + + long bindCount = adminUserRoleMapper.selectCount(new LambdaQueryWrapper() + .eq(AdminUserRole::getRoleId, id)); + if (bindCount > 0) { + return Result.error("该角色已分配给管理员,无法删除"); + } + + adminRolePermissionMapper.delete(new LambdaQueryWrapper() + .eq(AdminRolePermission::getRoleId, id)); + adminRoleMapper.deleteById(id); + return Result.success(); + } + + private boolean existsRoleName(String name, Long excludeId) { + return adminRoleMapper.selectCount(new LambdaQueryWrapper() + .eq(AdminRole::getName, name) + .ne(excludeId != null, AdminRole::getId, excludeId)) > 0; + } + + private boolean existsRoleCode(String code, Long excludeId) { + return adminRoleMapper.selectCount(new LambdaQueryWrapper() + .eq(AdminRole::getCode, code) + .ne(excludeId != null, AdminRole::getId, excludeId)) > 0; + } + + private void replaceRolePermissions(Long roleId, List permissionIds, String operator) { + adminRolePermissionMapper.delete(new LambdaQueryWrapper() + .eq(AdminRolePermission::getRoleId, roleId)); + saveRolePermissions(roleId, permissionIds, operator); + } + + private void saveRolePermissions(Long roleId, List permissionIds, String operator) { + if (permissionIds == null || permissionIds.isEmpty()) { + return; + } + List 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 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; + } +} diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java index ac0f798..354b4ba 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java @@ -53,14 +53,31 @@ public class AdminUserServiceImpl extends ServiceImpl> listUsers(Page page, String keyword) { - Page userPage = adminUserMapper.selectUserList(page, keyword); + LambdaQueryWrapper 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 userPage = page(page, queryWrapper); // 转换为VO Page voPage = new Page<>(); BeanUtils.copyProperties(userPage, voPage, "records"); List userVOList = new ArrayList<>(); - for (AdminUser user : userPage.getRecords()) { + for (AdminUser baseUser : userPage.getRecords()) { + AdminUser user = adminUserMapper.selectUserDetail(baseUser.getId()); + if (user == null) { + user = baseUser; + } AdminUserVO vo = new AdminUserVO(); BeanUtils.copyProperties(user, vo); diff --git a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java index 729ee87..bd9a3bf 100644 --- a/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java +++ b/backend/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java @@ -7,6 +7,7 @@ import com.nuaa.video.admin.dto.LoginDTO; import com.nuaa.video.admin.entity.AdminPermission; import com.nuaa.video.admin.entity.AdminRole; import com.nuaa.video.admin.entity.AdminUser; +import com.nuaa.video.admin.mapper.AdminUserMapper; import com.nuaa.video.admin.security.AdminUserDetails; import com.nuaa.video.admin.security.JwtTokenProvider; import com.nuaa.video.admin.service.AdminUserService; @@ -44,6 +45,7 @@ public class AuthServiceImpl implements AuthService { private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; private final AdminUserService adminUserService; + private final AdminUserMapper adminUserMapper; private final RedisTemplate redisTemplate; @Value("${login.retry-count:5}") @@ -169,7 +171,7 @@ public class AuthServiceImpl implements AuthService { @Override public Result getCurrentUser(AdminUserDetails adminUserDetails) { // 获取用户信息 - AdminUser adminUser = adminUserService.getById(adminUserDetails.getId()); + AdminUser adminUser = adminUserMapper.selectUserDetail(adminUserDetails.getId()); if (adminUser == null) { return Result.error("用户不存在"); } diff --git a/backend/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java b/backend/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java index 82d8ad3..ce30729 100644 --- a/backend/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java +++ b/backend/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java @@ -1,5 +1,6 @@ package com.nuaa.video.auth.controller; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nuaa.video.auth.entity.AuthUser; import com.nuaa.video.auth.service.AuthUserService; @@ -10,6 +11,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.stream.Collectors; + /** * 认证管理控制器 - 供管理后台调用 */ @@ -63,6 +67,26 @@ public class AdminAuthController { return authUserService.lockUser(userId, locked); } + /** + * 按账号状态查询用户ID - 管理接口 + */ + @GetMapping("/user-ids/status/{status}") + public Result> getUserIdsByStatus( + @PathVariable Integer status, + HttpServletRequest request) { + if (!isInternalRequest(request)) { + return Result.error("非法的内部调用"); + } + + List userIds = authUserService.list(new LambdaQueryWrapper() + .select(AuthUser::getId) + .eq(AuthUser::getStatus, status)) + .stream() + .map(AuthUser::getId) + .collect(Collectors.toList()); + return Result.success(userIds); + } + private boolean isInternalRequest(HttpServletRequest request) { return internalServiceToken.equals(request.getHeader("X-Internal-Token")); } diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java index 3cf8861..61fa30f 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java @@ -3,6 +3,7 @@ package com.nuaa.video.comment.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nuaa.video.comment.entity.Comment; import com.nuaa.video.comment.service.CommentService; +import com.nuaa.video.comment.vo.AdminCommentVO; import com.nuaa.video.comment.vo.CommentStatVO; import com.nuaa.video.comment.vo.CommentVO; import com.nuaa.video.common.model.vo.Result; @@ -30,18 +31,19 @@ public class AdminCommentController { * 获取评论列表 - 管理接口 */ @GetMapping("/list") - public Result> getCommentList( + public Result> getCommentList( @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "videoId", required = false) Long videoId, @RequestParam(value = "userId", required = false) Long userId, + @RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "keyword", required = false) String keyword, HttpServletRequest request) { if (!isInternalRequest(request)) { return Result.error("非法的内部调用"); } - return commentService.getAdminCommentList(page, size, videoId, userId, keyword); + return commentService.getAdminCommentList(page, size, videoId, userId, status, keyword); } /** @@ -66,6 +68,17 @@ public class AdminCommentController { return commentService.adminDeleteComment(commentId); } + /** + * 恢复评论 - 管理接口 + */ + @PutMapping("/{commentId}/restore") + public Result restoreComment(@PathVariable Long commentId, HttpServletRequest request) { + if (!isInternalRequest(request)) { + return Result.error("非法的内部调用"); + } + return commentService.adminRestoreComment(commentId); + } + /** * 获取评论统计信息 - 管理接口 */ diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClient.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClient.java index ea51f4e..700141d 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClient.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClient.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; /** * 用户服务Feign客户端 @@ -35,5 +36,7 @@ public interface UserFeignClient { @PostMapping("/user/notification/send/{userId}/{type}") Result 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); } diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClientFallbackFactory.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClientFallbackFactory.java index a1447f3..c32c7d9 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClientFallbackFactory.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/feign/UserFeignClientFallbackFactory.java @@ -23,7 +23,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory sendNotification(Long userId, Integer type, String internalToken) { + public Result sendNotification(Long userId, Integer type, String content, Long relatedId, String internalToken) { log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type); return Result.error("发送用户通知失败"); } diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java index 3d6eda4..a8a8840 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java @@ -7,6 +7,7 @@ import com.nuaa.video.comment.dto.CommentQueryDTO; import com.nuaa.video.comment.entity.Comment; import com.nuaa.video.comment.vo.CommentStatVO; import com.nuaa.video.comment.vo.CommentVO; +import com.nuaa.video.comment.vo.AdminCommentVO; import com.nuaa.video.common.model.vo.Result; /** @@ -99,7 +100,7 @@ public interface CommentService extends IService { /** * 获取评论列表 - 管理接口 */ - Result> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword); + Result> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword); /** * 获取评论详情 - 管理接口 @@ -111,8 +112,13 @@ public interface CommentService extends IService { */ Result adminDeleteComment(Long commentId); + /** + * 管理员恢复评论 - 管理接口 + */ + Result adminRestoreComment(Long commentId); + /** * 获取评论统计信息 - 管理接口 */ Result getCommentStats(); -} \ No newline at end of file +} diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentReplyServiceImpl.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentReplyServiceImpl.java index d734975..fc36585 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentReplyServiceImpl.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentReplyServiceImpl.java @@ -277,10 +277,10 @@ public class CommentReplyServiceImpl extends ServiceImpl { try { if (!userId.equals(comment.getUserId())) { - userFeignClient.sendNotification(comment.getUserId(), 2, internalServiceToken); + userFeignClient.sendNotification(comment.getUserId(), 2, null, replyDTO.getVideoId(), internalServiceToken); } if (replyDTO.getReplyToId() != null && !userId.equals(replyDTO.getReplyToId())) { - userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, internalServiceToken); + userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, null, replyDTO.getVideoId(), internalServiceToken); } } catch (Exception e) { log.warn("发送回复通知失败", e); diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java index a40acbf..a568e15 100644 --- a/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java @@ -17,6 +17,7 @@ import com.nuaa.video.comment.service.CommentService; import com.nuaa.video.comment.utils.SensitiveWordFilter; import com.nuaa.video.comment.vo.CommentStatVO; import com.nuaa.video.comment.vo.CommentVO; +import com.nuaa.video.comment.vo.AdminCommentVO; import com.nuaa.video.comment.vo.ReplyVO; import com.nuaa.video.common.model.vo.Result; import lombok.RequiredArgsConstructor; @@ -94,7 +95,7 @@ public class CommentServiceImpl extends ServiceImpl impl updateVideoCommentCountAsync(commentDTO.getVideoId(), 1); - sendCommentNotificationAsync(videoResult.getData(), userId); + sendCommentNotificationAsync(videoResult.getData(), userId, commentDTO.getVideoId()); return Result.success(comment.getId()); } @@ -361,7 +362,7 @@ public class CommentServiceImpl extends ServiceImpl impl } @Override - public Result> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword) { + public Result> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword) { if (page == null || page < 1) { page = 1; } @@ -372,7 +373,9 @@ public class CommentServiceImpl extends ServiceImpl impl // 构建查询条件 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - // 管理员可以查看所有状态的评论,包括已删除的 + if (status != null) { + queryWrapper.eq(Comment::getStatus, status); + } // 按视频ID筛选 if (videoId != null) { @@ -397,12 +400,12 @@ public class CommentServiceImpl extends ServiceImpl impl commentPage = page(commentPage, queryWrapper); // 转换为VO - Page voPage = new Page<>(); + Page voPage = new Page<>(); BeanUtils.copyProperties(commentPage, voPage, "records"); - List commentVOList = new ArrayList<>(); + List commentVOList = new ArrayList<>(); for (Comment comment : commentPage.getRecords()) { - CommentVO vo = convertToCommentVO(comment, null); + AdminCommentVO vo = convertToAdminCommentVO(comment); commentVOList.add(vo); } @@ -453,6 +456,23 @@ public class CommentServiceImpl extends ServiceImpl impl return Result.success(); } + @Override + @Transactional(rollbackFor = Exception.class) + public Result adminRestoreComment(Long commentId) { + Comment comment = getById(commentId); + if (comment == null) { + return Result.error("评论不存在"); + } + if (comment.getStatus() == 1) { + return Result.success(); + } + + commentMapper.updateCommentStatus(commentId, 1, "admin"); + updateVideoCommentCountAsync(comment.getVideoId(), 1); + + return Result.success(); + } + @Override public Result getCommentStats() { CommentStatVO statVO = new CommentStatVO(); @@ -514,14 +534,44 @@ public class CommentServiceImpl extends ServiceImpl impl return vo; } - private void sendCommentNotificationAsync(Object videoPayload, Long userId) { + private AdminCommentVO convertToAdminCommentVO(Comment comment) { + AdminCommentVO vo = new AdminCommentVO(); + BeanUtils.copyProperties(comment, vo); + + try { + Result userResult = userFeignClient.getUserProfile(comment.getUserId()); + if (userResult.getCode() == 200 && userResult.getData() != null) { + @SuppressWarnings("unchecked") + Map userData = (Map) 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 videoResult = contentFeignClient.getVideoInfo(comment.getVideoId()); + if (videoResult.getCode() == 200 && videoResult.getData() != null) { + @SuppressWarnings("unchecked") + Map videoData = (Map) videoResult.getData(); + vo.setVideoTitle((String) videoData.get("title")); + } + } catch (Exception e) { + log.error("管理端获取视频信息失败, videoId={}", comment.getVideoId(), e); + } + + return vo; + } + + private void sendCommentNotificationAsync(Object videoPayload, Long userId, Long videoId) { CompletableFuture.runAsync(() -> { try { @SuppressWarnings("unchecked") Map videoData = (Map) videoPayload; Long videoAuthorId = Long.valueOf(videoData.get("userId").toString()); if (!userId.equals(videoAuthorId)) { - userFeignClient.sendNotification(videoAuthorId, 2, internalServiceToken); + userFeignClient.sendNotification(videoAuthorId, 2, null, videoId, internalServiceToken); } } catch (Exception e) { log.warn("发送评论通知失败", e); diff --git a/backend/video-comment/src/main/java/com/nuaa/video/comment/vo/AdminCommentVO.java b/backend/video-comment/src/main/java/com/nuaa/video/comment/vo/AdminCommentVO.java new file mode 100644 index 0000000..afc6954 --- /dev/null +++ b/backend/video-comment/src/main/java/com/nuaa/video/comment/vo/AdminCommentVO.java @@ -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; +} diff --git a/backend/video-comment/src/test/java/com/nuaa/video/comment/service/NotificationDispatchTest.java b/backend/video-comment/src/test/java/com/nuaa/video/comment/service/NotificationDispatchTest.java new file mode 100644 index 0000000..71dffa7 --- /dev/null +++ b/backend/video-comment/src/test/java/com/nuaa/video/comment/service/NotificationDispatchTest.java @@ -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")); + } +} diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java b/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java index a0f8e28..bd0df29 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/controller/VideoController.java @@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nuaa.video.common.model.vo.Result; import com.nuaa.video.content.dto.*; import com.nuaa.video.content.entity.ContentVideo; +import com.nuaa.video.content.entity.ContentVideoPlayRecord; import com.nuaa.video.content.mapper.ContentVideoMapper; import com.nuaa.video.content.service.VideoService; +import com.nuaa.video.content.vo.PlayHistoryVO; import com.nuaa.video.content.vo.UploadResultVO; import com.nuaa.video.content.vo.VideoInfoVO; import com.nuaa.video.content.vo.VideoListVO; @@ -62,14 +64,7 @@ public class VideoController { @GetMapping("/public/{id}") public Result getVideoInfoPublic(@PathVariable Long id, HttpServletRequest request) { Long userId = getUserIdFromRequest(request); - Result result = videoService.getVideoInfo(id, userId); - if (result.getData() == null) { - return result; - } - if (result.getData().getPublishTime() == null) { - return Result.error("视频暂未公开"); - } - return result; + return videoService.getVideoInfo(id, userId); } /** @@ -223,6 +218,34 @@ public class VideoController { return videoService.playVideo(recordDTO, userId); } + /** + * 播放历史 + */ + @GetMapping("/history") + public Result> 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 page = new Page<>(pageNum, pageSize); + return videoService.getPlayHistory(page, userId); + } + + /** + * 清空播放历史 + */ + @DeleteMapping("/history") + public Result clearPlayHistory(HttpServletRequest request) { + Long userId = getUserIdFromRequest(request); + if (userId == null) { + return Result.error("未获取到用户ID"); + } + return videoService.clearPlayHistory(userId); + } + /** * 获取用户视频列表 */ diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClient.java b/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClient.java index f417a06..0248ca6 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClient.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClient.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; /** * 用户服务Feign客户端 @@ -57,4 +58,30 @@ public interface UserFeignClient { Result increaseFavoriteCount(@PathVariable("userId") Long userId, @PathVariable("count") Integer count, @RequestHeader("X-Internal-Token") String internalToken); + + /** + * 增加用户硬币余额 + */ + @PostMapping("/user/profile/count/coin/increase/{userId}/{count}") + Result increaseCoinCount(@PathVariable("userId") Long userId, + @PathVariable("count") Integer count, + @RequestHeader("X-Internal-Token") String internalToken); + + /** + * 扣减用户硬币余额 + */ + @PostMapping("/user/profile/count/coin/decrease/{userId}/{count}") + Result decreaseCoinCount(@PathVariable("userId") Long userId, + @PathVariable("count") Integer count, + @RequestHeader("X-Internal-Token") String internalToken); + + /** + * 发送用户通知 + */ + @PostMapping("/user/notification/send/{userId}/{type}") + Result 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); } diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClientFallbackFactory.java b/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClientFallbackFactory.java index 4b07aef..87bb2a6 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClientFallbackFactory.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/feign/UserFeignClientFallbackFactory.java @@ -39,6 +39,24 @@ public class UserFeignClientFallbackFactory implements FallbackFactory increaseCoinCount(Long userId, Integer count, String internalToken) { + log.error("增加用户硬币余额失败,用户ID:{}, 数量:{}", userId, count); + return Result.error("更新用户硬币余额失败"); + } + + @Override + public Result decreaseCoinCount(Long userId, Integer count, String internalToken) { + log.error("扣减用户硬币余额失败,用户ID:{}, 数量:{}", userId, count); + return Result.error("扣减用户硬币余额失败"); + } + + @Override + public Result sendNotification(Long userId, Integer type, String content, Long relatedId, String internalToken) { + log.error("发送用户通知失败,用户ID:{}, 类型:{}, relatedId:{}", userId, type, relatedId); + return Result.error("发送用户通知失败"); + } }; } } diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java b/backend/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java index 36dd450..c091601 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.nuaa.video.common.model.vo.Result; import com.nuaa.video.content.dto.*; import com.nuaa.video.content.entity.ContentVideo; +import com.nuaa.video.content.entity.ContentVideoPlayRecord; +import com.nuaa.video.content.vo.PlayHistoryVO; import com.nuaa.video.content.vo.UploadResultVO; import com.nuaa.video.content.vo.VideoInfoVO; import com.nuaa.video.content.vo.VideoListVO; @@ -135,6 +137,23 @@ public interface VideoService extends IService { */ Result playVideo(PlayRecordDTO recordDTO, Long userId); + /** + * 获取播放历史 + * + * @param page 分页参数 + * @param userId 用户ID + * @return 播放历史 + */ + Result> getPlayHistory(Page page, Long userId); + + /** + * 清空播放历史 + * + * @param userId 用户ID + * @return 结果 + */ + Result clearPlayHistory(Long userId); + /** * 获取用户视频列表 * diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/FavoriteServiceImpl.java b/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/FavoriteServiceImpl.java index a5f657a..78f7969 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/FavoriteServiceImpl.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/FavoriteServiceImpl.java @@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -52,6 +53,8 @@ public class FavoriteServiceImpl extends ServiceImpl { FavoriteVO vo = new FavoriteVO(); BeanUtils.copyProperties(favorite, vo); + int visibleVideoCount = countVisibleFavoriteVideos(favorite.getId()); + vo.setVideoCount(visibleVideoCount); return vo; }) .collect(Collectors.toList()); @@ -192,4 +195,23 @@ public class FavoriteServiceImpl extends ServiceImpl videoIds = videoFavoriteMapper.selectList( + new LambdaQueryWrapper() + .eq(ContentVideoFavorite::getFavoriteId, favoriteId) + ).stream().map(ContentVideoFavorite::getVideoId).collect(Collectors.toList()); + + if (videoIds.isEmpty()) { + return 0; + } + + Long count = videoMapper.selectCount( + new LambdaQueryWrapper() + .in(ContentVideo::getId, videoIds) + .eq(ContentVideo::getStatus, 1) + ); + + return Math.toIntExact(Objects.requireNonNullElse(count, 0L)); + } +} diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java b/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java index 9016996..f55eeee 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java @@ -17,6 +17,7 @@ import com.nuaa.video.content.service.TagService; import com.nuaa.video.content.service.VideoService; import com.nuaa.video.content.utils.VideoProcessor; import com.nuaa.video.content.vo.TagVO; +import com.nuaa.video.content.vo.PlayHistoryVO; import com.nuaa.video.content.vo.UploadResultVO; import com.nuaa.video.content.vo.VideoInfoVO; import com.nuaa.video.content.vo.VideoListVO; @@ -32,6 +33,7 @@ import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -48,6 +50,8 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class VideoServiceImpl extends ServiceImpl implements VideoService { + private static final ZoneId APP_ZONE_ID = ZoneId.of("Asia/Shanghai"); + @Value("${service.auth.token}") private String internalServiceToken; @@ -164,6 +168,12 @@ public class VideoServiceImpl extends ServiceImpl 0); + + ContentVideoPlayRecord playRecord = getUserPlayRecord(userId, id); + if (playRecord != null) { + videoInfoVO.setPlayProgress(playRecord.getPlayTime()); + videoInfoVO.setIsPlayFinished(Objects.equals(playRecord.getIsFinished(), 1)); + } else { + videoInfoVO.setPlayProgress(0); + videoInfoVO.setIsPlayFinished(false); + } } return Result.success(videoInfoVO); @@ -409,6 +428,7 @@ public class VideoServiceImpl extends ServiceImpl coinBalanceResult = userFeignClient.decreaseCoinCount(userId, amount, internalServiceToken); + if (coinBalanceResult == null || coinBalanceResult.getCode() != 200) { + String message = coinBalanceResult != null ? coinBalanceResult.getMessage() : "扣减硬币失败"; + return Result.error(message); + } - // 发送统计消息,增加投币数 - videoMessageProducer.sendVideoStatsMessage( - new VideoMessageProducer.VideoStatsMessage() {{ - setVideoId(videoId); - setUserId(userId); - setAction("coin"); - setCount(amount); - }} - ); + try { + // 添加投币记录 + ContentVideoCoin coin = new ContentVideoCoin(); + coin.setVideoId(videoId); + coin.setUserId(userId); + coin.setAmount(amount); + coin.setCreateTime(LocalDateTime.now()); + videoCoinMapper.insert(coin); - return Result.success(); + // 发送统计消息,增加投币数 + videoMessageProducer.sendVideoStatsMessage( + new VideoMessageProducer.VideoStatsMessage() {{ + setVideoId(videoId); + setUserId(userId); + setAction("coin"); + setCount(amount); + }} + ); + + return Result.success(); + } catch (Exception e) { + Result rollbackResult = userFeignClient.increaseCoinCount(userId, amount, internalServiceToken); + if (rollbackResult == null || rollbackResult.getCode() != 200) { + log.error("投币失败且硬币余额补偿失败,videoId={}, userId={}, amount={}, rollbackResult={}", + videoId, userId, amount, rollbackResult); + } + throw e; + } } @Override @@ -673,6 +708,54 @@ public class VideoServiceImpl extends ServiceImpl> getPlayHistory(Page page, Long userId) { + Page recordPage = playRecordMapper.selectPage( + page, + new LambdaQueryWrapper() + .eq(ContentVideoPlayRecord::getUserId, userId) + .orderByDesc(ContentVideoPlayRecord::getUpdateTime) + ); + + List 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 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 clearPlayHistory(Long userId) { + playRecordMapper.delete( + new LambdaQueryWrapper() + .eq(ContentVideoPlayRecord::getUserId, userId) + ); + return Result.success(); + } + @Override public Result> getUserVideos(Long userId, Page page, Long currentUserId) { VideoQueryDTO queryDTO = new VideoQueryDTO(); @@ -885,7 +968,7 @@ public class VideoServiceImpl extends ServiceImpl() .ge(ContentVideo::getCreateTime, today.atStartOfDay())); stats.put("todayNewVideos", todayNewVideos); @@ -950,6 +1033,26 @@ public class VideoServiceImpl extends ServiceImpl() + .eq(ContentVideoPlayRecord::getUserId, userId) + .eq(ContentVideoPlayRecord::getVideoId, videoId) + ); + } + + private void sendVideoLikeNotificationAsync(ContentVideo video, Long operatorUserId) { + runAfterCommitAsync("发送视频点赞通知", () -> { + if (!operatorUserId.equals(video.getUserId())) { + Result notifyResult = userFeignClient.sendNotification(video.getUserId(), 1, null, video.getId(), internalServiceToken); + if (notifyResult == null || notifyResult.getCode() != 200) { + log.warn("发送视频点赞通知失败,videoId={}, ownerUserId={}, operatorUserId={}, result={}", + video.getId(), video.getUserId(), operatorUserId, notifyResult); + } + } + }); + } + private void runAfterCommitAsync(String action, Runnable task) { Runnable wrappedTask = () -> CompletableFuture.runAsync(() -> { try { diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/vo/PlayHistoryVO.java b/backend/video-content/src/main/java/com/nuaa/video/content/vo/PlayHistoryVO.java new file mode 100644 index 0000000..51b7cbd --- /dev/null +++ b/backend/video-content/src/main/java/com/nuaa/video/content/vo/PlayHistoryVO.java @@ -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; +} diff --git a/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java b/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java index b53063d..17da33e 100644 --- a/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java +++ b/backend/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java @@ -68,6 +68,16 @@ public class VideoInfoVO implements Serializable { */ private Integer height; + /** + * 视频格式 + */ + private String format; + + /** + * 文件大小 + */ + private Long size; + /** * 分类ID */ @@ -123,6 +133,11 @@ public class VideoInfoVO implements Serializable { */ private Integer danmakuCount; + /** + * 视频状态 + */ + private Integer status; + /** * 是否已点赞 */ @@ -138,11 +153,31 @@ public class VideoInfoVO implements Serializable { */ private Boolean isCoined; + /** + * 播放进度(秒) + */ + private Integer playProgress; + + /** + * 是否已看完 + */ + private Boolean isPlayFinished; + /** * 发布时间 */ private LocalDateTime publishTime; + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + /** * 审核员ID */ @@ -162,4 +197,4 @@ public class VideoInfoVO implements Serializable { * 创建时间 */ private LocalDateTime reviewTime; -} \ No newline at end of file +} diff --git a/backend/video-content/src/test/java/com/nuaa/video/content/service/NotificationDispatchTest.java b/backend/video-content/src/test/java/com/nuaa/video/content/service/NotificationDispatchTest.java new file mode 100644 index 0000000..8895b3e --- /dev/null +++ b/backend/video-content/src/test/java/com/nuaa/video/content/service/NotificationDispatchTest.java @@ -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"); + } +} diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java b/backend/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java index 94585da..e7d38bd 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java @@ -33,12 +33,13 @@ public class AdminUserController { @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "status", required = false) Integer status, HttpServletRequest request) { if (!isInternalRequest(request)) { return Result.error("非法的内部调用"); } Page pageParam = new Page<>(page, size); - return userInfoService.getAdminUserList(pageParam, keyword); + return userInfoService.getAdminUserList(pageParam, keyword, status); } /** diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/controller/UserProfileController.java b/backend/video-user/src/main/java/com/nuaa/video/user/controller/UserProfileController.java index c100c2b..15e4bf2 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/controller/UserProfileController.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/controller/UserProfileController.java @@ -172,6 +172,34 @@ public class UserProfileController { return userInfoService.increaseFavoriteCount(userId, count); } + /** + * 增加用户硬币余额 + */ + @PostMapping("/count/coin/increase/{userId}/{count}") + public Result 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 decreaseCoinCount( + @PathVariable Long userId, + @PathVariable Integer count, + HttpServletRequest request) { + if (!isInternalRequest(request)) { + return Result.error("非法的内部调用"); + } + return userInfoService.decreaseCoinCount(userId, count); + } + /** * 从请求中获取用户名 */ diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java b/backend/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java index 0bfa8f1..a939369 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java @@ -4,6 +4,8 @@ import com.nuaa.video.common.model.vo.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; +import java.util.List; + /** * 认证服务Feign客户端 */ @@ -44,4 +46,11 @@ public interface AuthFeignClient { @PathVariable Long userId, @RequestParam String newPassword, @RequestHeader("X-Internal-Token") String internalToken); + + /** + * 按账号状态查询用户ID - 管理接口 + */ + @GetMapping("/admin/user-ids/status/{status}") + Result> getUserIdsByStatus(@PathVariable Integer status, + @RequestHeader("X-Internal-Token") String internalToken); } diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/mapper/UserInfoMapper.java b/backend/video-user/src/main/java/com/nuaa/video/user/mapper/UserInfoMapper.java index bbe27d6..75f5d3f 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/mapper/UserInfoMapper.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/mapper/UserInfoMapper.java @@ -43,4 +43,14 @@ public interface UserInfoMapper extends BaseMapper { * 增加收藏数 */ 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); } diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java b/backend/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java index f95e8b1..d455b7e 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java @@ -93,10 +93,20 @@ public interface UserInfoService extends IService { */ Result increaseFavoriteCount(Long userId, Integer count); + /** + * 增加用户硬币余额 + */ + Result increaseCoinCount(Long userId, Integer count); + + /** + * 扣减用户硬币余额 + */ + Result decreaseCoinCount(Long userId, Integer count); + /** * 获取用户列表 - 管理接口 */ - Result> getAdminUserList(Page page, String keyword); + Result> getAdminUserList(Page page, String keyword, Integer status); /** * 获取用户详情 - 管理接口 diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java b/backend/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java index ba98edd..91baa29 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java @@ -27,6 +27,8 @@ import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,9 +43,15 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService { + private static final ZoneId APP_ZONE_ID = ZoneId.of("Asia/Shanghai"); + private static final long DEFAULT_AVATAR_MAX_SIZE_BYTES = 5L * 1024 * 1024; + @Value("${service.auth.token}") private String internalServiceToken; + @Value("${user.avatar.max-size-bytes:" + DEFAULT_AVATAR_MAX_SIZE_BYTES + "}") + private long avatarMaxSizeBytes; + private final UserInfoMapper userInfoMapper; private final UserFollowMapper userFollowMapper; private final UserSettingService userSettingService; @@ -118,6 +126,19 @@ public class UserInfoServiceImpl extends ServiceImpl i return Result.error(ResultCode.USER_NOT_FOUND.getCode(),ResultCode.USER_NOT_FOUND.getMessage()); } + if (file == null || file.isEmpty()) { + return Result.error("请选择头像文件"); + } + + String contentType = file.getContentType(); + if (!StringUtils.hasText(contentType) || !contentType.startsWith("image/")) { + return Result.error("仅支持上传图片格式头像"); + } + + if (file.getSize() > avatarMaxSizeBytes) { + return Result.error("头像文件不能超过5MB"); + } + // 上传头像 String avatarUrl = minioUtils.uploadImage(file); @@ -127,8 +148,8 @@ public class UserInfoServiceImpl extends ServiceImpl i userInfo.setUpdateTime(LocalDateTime.now()); updateById(userInfo); - // 返回头像URL - return Result.success(minioUtils.getPreSignedUrl(avatarUrl)); + // 返回对象路径,由前端统一拼接可访问的媒体地址 + return Result.success(avatarUrl); } catch (Exception e) { log.error("上传头像失败", e); return Result.error("上传头像失败: " + e.getMessage()); @@ -154,7 +175,7 @@ public class UserInfoServiceImpl extends ServiceImpl i userInfo.setFollowingCount(0); userInfo.setVideoCount(0); userInfo.setLikeCount(0); - userInfo.setCoinCount(0); + userInfo.setCoinCount(20); userInfo.setFavoriteCount(0); userInfo.setViewCount(0L); userInfo.setLevel(1); @@ -247,14 +268,64 @@ public class UserInfoServiceImpl extends ServiceImpl i } @Override - public Result> getAdminUserList(Page page, String keyword) { + public Result 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 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> getAdminUserList(Page page, String keyword, Integer status) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); // 添加关键词搜索条件 if (StringUtils.hasText(keyword)) { - queryWrapper.like(UserInfo::getNickname, keyword) + queryWrapper.and(wrapper -> wrapper + .like(UserInfo::getNickname, keyword) .or() - .like(UserInfo::getSignature, keyword); + .like(UserInfo::getSignature, keyword)); + } + + if (status != null) { + Result> userIdsResult = authFeignClient.getUserIdsByStatus(status, internalServiceToken); + List userIds = userIdsResult.getData() == null ? Collections.emptyList() : userIdsResult.getData(); + if (userIds.isEmpty()) { + Page 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 i stats.put("totalUsers", totalUsers); // 今日新增用户 - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(APP_ZONE_ID); long todayNewUsers = count(new LambdaQueryWrapper() .ge(UserInfo::getCreateTime, today.atStartOfDay())); stats.put("todayNewUsers", todayNewUsers); diff --git a/backend/video-user/src/main/java/com/nuaa/video/user/vo/UserProfileVO.java b/backend/video-user/src/main/java/com/nuaa/video/user/vo/UserProfileVO.java index bfc1f5d..d895371 100644 --- a/backend/video-user/src/main/java/com/nuaa/video/user/vo/UserProfileVO.java +++ b/backend/video-user/src/main/java/com/nuaa/video/user/vo/UserProfileVO.java @@ -68,6 +68,11 @@ public class UserProfileVO implements Serializable { */ private Integer likeCount; + /** + * 硬币余额 + */ + private Integer coinCount; + /** * 等级 */ @@ -92,4 +97,4 @@ public class UserProfileVO implements Serializable { * 创建时间 */ private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/backend/video-user/src/main/resources/mapper/UserInfoMapper.xml b/backend/video-user/src/main/resources/mapper/UserInfoMapper.xml index fc8e580..fe569c3 100644 --- a/backend/video-user/src/main/resources/mapper/UserInfoMapper.xml +++ b/backend/video-user/src/main/resources/mapper/UserInfoMapper.xml @@ -37,4 +37,19 @@ WHERE id = #{userId} + + UPDATE user_info + SET coin_count = GREATEST(coin_count + #{count}, 0), + update_time = NOW() + WHERE id = #{userId} + + + + UPDATE user_info + SET coin_count = coin_count - #{count}, + update_time = NOW() + WHERE id = #{userId} + AND coin_count >= #{count} + +