fix(backend): Restore service flow and admin management integration

This commit is contained in:
2026-04-18 18:20:58 +08:00
parent 92a139e470
commit 3adb088eca
83 changed files with 1696 additions and 703 deletions
@@ -2,8 +2,11 @@ package com.nuaa.video.user.config;
import com.nuaa.video.user.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -15,11 +18,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Value("${service.auth.token}")
private String internalServiceToken;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
@@ -29,15 +36,27 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/user/profile/public/**",
"/user/profile/count/**",
"/user/profile/init",
"/user/profile/search",
"/user/follow/following/**",
"/user/follow/followers/**",
"/actuator/**",
"/error"
).permitAll()
.requestMatchers(
"/user/profile/count/**",
"/user/profile/init",
"/user/profile/admin/**",
"/user/notification/send/**"
).access((authentication, context) ->
new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token"))))
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
private boolean isInternalRequest(String internalToken) {
return internalServiceToken.equals(internalToken);
}
}
@@ -5,8 +5,10 @@ import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.user.entity.UserInfo;
import com.nuaa.video.user.service.UserInfoService;
import com.nuaa.video.user.vo.UserProfileVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
/**
@@ -18,6 +20,9 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
public class AdminUserController {
@Value("${service.auth.token}")
private String internalServiceToken;
private final UserInfoService userInfoService;
/**
@@ -27,7 +32,11 @@ public class AdminUserController {
public Result<Page<UserProfileVO>> getUserList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword) {
@RequestParam(value = "keyword", required = false) String keyword,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
Page<UserInfo> pageParam = new Page<>(page, size);
return userInfoService.getAdminUserList(pageParam, keyword);
}
@@ -36,7 +45,10 @@ public class AdminUserController {
* 获取用户详情 - 管理接口
*/
@GetMapping("/{userId}")
public Result<UserProfileVO> getUserDetail(@PathVariable Long userId) {
public Result<UserProfileVO> getUserDetail(@PathVariable Long userId, HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return userInfoService.getAdminUserDetail(userId);
}
@@ -46,8 +58,12 @@ public class AdminUserController {
@PutMapping("/status/{userId}/{status}")
public Result<Void> updateUserStatus(
@PathVariable Long userId,
@PathVariable Integer status) {
return userInfoService.updateUserStatus(userId, status);
@PathVariable Integer status,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return userInfoService.updateUserStatus(userId, status, internalServiceToken);
}
/**
@@ -56,15 +72,26 @@ public class AdminUserController {
@PutMapping("/reset-password/{userId}")
public Result<Void> resetUserPassword(
@PathVariable Long userId,
@RequestParam String newPassword) {
return userInfoService.resetUserPassword(userId, newPassword);
@RequestParam String newPassword,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return userInfoService.resetUserPassword(userId, newPassword, internalServiceToken);
}
/**
* 获取用户统计信息 - 管理接口
*/
@GetMapping("/stats")
public Result<Object> getUserStats() {
public Result<Object> getUserStats(HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return userInfoService.getUserStats();
}
}
private boolean isInternalRequest(HttpServletRequest request) {
return internalServiceToken.equals(request.getHeader("X-Internal-Token"));
}
}
@@ -8,10 +8,9 @@ import com.nuaa.video.user.vo.UserNotificationVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
/**
* 用户通知控制器
*/
@@ -21,6 +20,9 @@ import java.time.LocalDateTime;
@RequiredArgsConstructor
public class UserNotificationController {
@Value("${service.auth.token}")
private String internalServiceToken;
private final UserNotificationService userNotificationService;
/**
@@ -86,17 +88,10 @@ public class UserNotificationController {
@RequestParam(required = false) String content,
@RequestParam(required = false) Long relatedId,
HttpServletRequest request) {
// 验证调用者身份
Long currentUserId = getUserIdFromRequest(request);
if (currentUserId == null) {
return Result.error("未登录");
if (!internalServiceToken.equals(request.getHeader("X-Internal-Token"))) {
return Result.error("非法的内部调用");
}
// 创建通知对象
UserNotification notification = new UserNotification();
notification.setUserId(userId);
notification.setType(type);
// 设置通知内容
String notificationContent;
if (content != null && !content.isEmpty()) {
@@ -120,16 +115,7 @@ public class UserNotificationController {
notificationContent = "新通知";
}
}
notification.setContent(notificationContent);
notification.setRelatedId(relatedId);
notification.setIsRead(0); // 未读
notification.setCreateTime(LocalDateTime.now());
// 保存通知
userNotificationService.save(notification);
return Result.success();
return userNotificationService.createNotification(userId, type, notificationContent, relatedId);
}
/**
@@ -142,4 +128,4 @@ public class UserNotificationController {
}
return null;
}
}
}
@@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -25,6 +26,9 @@ import java.util.Map;
@RequiredArgsConstructor
public class UserProfileController {
@Value("${service.auth.token}")
private String internalServiceToken;
private final UserInfoService userInfoService;
private final AuthFeignClient authFeignClient;
@@ -33,7 +37,11 @@ public class UserProfileController {
*/
@PostMapping("/init")
public Result<Void> initUserInfo(@RequestParam("userId") Long userId,
@RequestParam("username") String username) {
@RequestParam("username") String username,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return userInfoService.createUserInfo(userId, username);
}
@@ -130,13 +138,9 @@ public class UserProfileController {
@PathVariable Long userId,
@PathVariable Integer count,
HttpServletRequest request) {
// 验证权限(实际可以通过更复杂的权限验证)
Long currentUserId = getUserIdFromRequest(request);
if (currentUserId == null) {
return Result.error("未登录");
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
// 调用service增加用户视频数量
return userInfoService.increaseVideoCount(userId, count);
}
@@ -148,13 +152,9 @@ public class UserProfileController {
@PathVariable Long userId,
@PathVariable Integer count,
HttpServletRequest request) {
// 验证权限
Long currentUserId = getUserIdFromRequest(request);
if (currentUserId == null) {
return Result.error("未登录");
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
// 调用service增加用户获赞数量
return userInfoService.increaseLikeCount(userId, count);
}
@@ -166,13 +166,9 @@ public class UserProfileController {
@PathVariable Long userId,
@PathVariable Integer count,
HttpServletRequest request) {
// 验证权限
Long currentUserId = getUserIdFromRequest(request);
if (currentUserId == null) {
return Result.error("未登录");
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
// 调用service增加用户收藏数量
return userInfoService.increaseFavoriteCount(userId, count);
}
@@ -197,4 +193,8 @@ public class UserProfileController {
}
return null;
}
}
private boolean isInternalRequest(HttpServletRequest request) {
return internalServiceToken.equals(request.getHeader("X-Internal-Token"));
}
}
@@ -33,7 +33,8 @@ public interface AuthFeignClient {
*/
@PutMapping("/admin/lock/{userId}/{locked}")
Result<Void> lockUser(@PathVariable Long userId,
@PathVariable Boolean locked);
@PathVariable Boolean locked,
@RequestHeader("X-Internal-Token") String internalToken);
/**
* 管理员重置用户密码 - 管理接口
@@ -41,5 +42,6 @@ public interface AuthFeignClient {
@PutMapping("/admin/reset-password/{userId}")
Result<Void> resetPassword(
@PathVariable Long userId,
@RequestParam String newPassword);
}
@RequestParam String newPassword,
@RequestHeader("X-Internal-Token") String internalToken);
}
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT认证过滤器(不依赖Feign)
@@ -29,7 +30,7 @@ import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
@Value("${jwt.secret}")
private String secret;
private static final String TOKEN_PREFIX = "Bearer ";
@@ -55,7 +56,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String username = claims.get("username", String.class);
// 设置用户权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<String> authorityClaims = claims.get("authorities", List.class);
List<SimpleGrantedAuthority> authorities = authorityClaims == null
? new ArrayList<>()
: authorityClaims.stream()
.filter(StringUtils::hasText)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toCollection(ArrayList::new));
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
@@ -70,6 +77,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 在请求属性中存储用户ID
request.setAttribute("userId", userId);
request.setAttribute("username", username);
if (!authorities.isEmpty()) {
request.setAttribute("adminId", userId);
}
log.debug("认证成功: 用户ID={}, 用户名={}", userId, username);
} catch (Exception e) {
@@ -99,4 +109,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
}
@@ -28,4 +28,19 @@ public interface UserInfoMapper extends BaseMapper<UserInfo> {
* @return 影响行数
*/
int increaseFollowerCount(@Param("userId") Long userId, @Param("count") int count);
}
/**
* 增加视频数
*/
int increaseVideoCount(@Param("userId") Long userId, @Param("count") int count);
/**
* 增加获赞数
*/
int increaseLikeCount(@Param("userId") Long userId, @Param("count") int count);
/**
* 增加收藏数
*/
int increaseFavoriteCount(@Param("userId") Long userId, @Param("count") int count);
}
@@ -3,10 +3,24 @@ package com.nuaa.video.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.user.entity.UserStatDaily;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 用户每日统计Mapper
*/
@Mapper
public interface UserStatDailyMapper extends BaseMapper<UserStatDaily> {
}
/**
* 原子更新用户每日统计,避免并发下先查后写导致锁等待。
*/
int upsertStatIncrement(@Param("userId") Long userId,
@Param("statDate") java.time.LocalDate statDate,
@Param("followerIncrement") int followerIncrement,
@Param("followingIncrement") int followingIncrement,
@Param("videoIncrement") int videoIncrement,
@Param("likeIncrement") int likeIncrement,
@Param("coinIncrement") int coinIncrement,
@Param("favoriteIncrement") int favoriteIncrement,
@Param("viewIncrement") long viewIncrement);
}
@@ -106,15 +106,15 @@ public interface UserInfoService extends IService<UserInfo> {
/**
* 更新用户状态 - 管理接口
*/
Result<Void> updateUserStatus(Long userId, Integer status);
Result<Void> updateUserStatus(Long userId, Integer status, String token);
/**
* 重置用户密码 - 管理接口
*/
Result<Void> resetUserPassword(Long userId, String newPassword);
Result<Void> resetUserPassword(Long userId, String newPassword, String token);
/**
* 获取用户统计信息 - 管理接口
*/
Result<Object> getUserStats();
}
}
@@ -19,6 +19,7 @@ import com.nuaa.video.user.vo.UserProfileVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -29,6 +30,7 @@ import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
@@ -39,6 +41,9 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Value("${service.auth.token}")
private String internalServiceToken;
private final UserInfoMapper userInfoMapper;
private final UserFollowMapper userFollowMapper;
private final UserSettingService userSettingService;
@@ -199,7 +204,6 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> increaseVideoCount(Long userId, Integer count) {
// 验证用户是否存在
UserInfo userInfo = getById(userId);
@@ -207,19 +211,14 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
return Result.error(ResultCode.USER_NOT_FOUND.getCode(),ResultCode.USER_NOT_FOUND.getMessage());
}
// 更新视频数量
userInfo.setVideoCount(userInfo.getVideoCount() + count);
userInfo.setUpdateTime(LocalDateTime.now());
updateById(userInfo);
userInfoMapper.increaseVideoCount(userId, count);
// 更新每日统计
updateUserStatDaily(userId, "video", count);
updateUserStatDailyAsync(userId, "video", count);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> increaseLikeCount(Long userId, Integer count) {
// 验证用户是否存在
UserInfo userInfo = getById(userId);
@@ -227,16 +226,13 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
return Result.error(ResultCode.USER_NOT_FOUND.getCode(),ResultCode.USER_NOT_FOUND.getMessage());
}
// 更新获赞数量
userInfo.setLikeCount(userInfo.getLikeCount() + count);
userInfo.setUpdateTime(LocalDateTime.now());
updateById(userInfo);
userInfoMapper.increaseLikeCount(userId, count);
updateUserStatDailyAsync(userId, "like", count);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> increaseFavoriteCount(Long userId, Integer count) {
// 验证用户是否存在
UserInfo userInfo = getById(userId);
@@ -244,10 +240,8 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
return Result.error(ResultCode.USER_NOT_FOUND.getCode(),ResultCode.USER_NOT_FOUND.getMessage());
}
// 更新收藏数量
userInfo.setFavoriteCount(userInfo.getFavoriteCount() + count);
userInfo.setUpdateTime(LocalDateTime.now());
updateById(userInfo);
userInfoMapper.increaseFavoriteCount(userId, count);
updateUserStatDailyAsync(userId, "favorite", count);
return Result.success();
}
@@ -306,7 +300,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateUserStatus(Long userId, Integer status) {
public Result<Void> updateUserStatus(Long userId, Integer status, String token) {
// 查询用户是否存在
UserInfo userInfo = getById(userId);
if (userInfo == null) {
@@ -320,19 +314,14 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
updateUser.setUpdatedBy("admin");
updateById(updateUser);
// 当禁用用户,需要清除用户相关的登录态等
if (status == 0) {
// 发送事件通知auth服务禁用该用户账号
//TODO 可以优化为消息队列
authFeignClient.lockUser(userId, true);
}
authFeignClient.lockUser(userId, status == 0, internalServiceToken);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> resetUserPassword(Long userId, String newPassword) {
public Result<Void> resetUserPassword(Long userId, String newPassword, String token) {
// 查询用户是否存在
UserInfo userInfo = getById(userId);
if (userInfo == null) {
@@ -340,7 +329,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
}
// 调用auth服务重置密码(实际项目中通过Feign或消息队列)
authFeignClient.resetPassword(userId, newPassword);
authFeignClient.resetPassword(userId, newPassword, internalServiceToken);
log.info("重置用户密码: userId={}, newPassword=***", userId);
@@ -362,9 +351,21 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
.ge(UserInfo::getCreateTime, today.atStartOfDay()));
stats.put("todayNewUsers", todayNewUsers);
// 活跃用户数(最近7天有登录记录的用户)
// 此处需要结合用户登录记录表查询,简化处理
stats.put("activeUsers", totalUsers * 0.3); // 示例数据
LocalDate activeSince = today.minusDays(6);
List<UserStatDaily> activeStats = userStatDailyMapper.selectList(new LambdaQueryWrapper<UserStatDaily>()
.ge(UserStatDaily::getStatDate, activeSince));
long activeUsers = activeStats.stream()
.filter(stat -> positive(stat.getFollowerIncrement())
|| positive(stat.getFollowingIncrement())
|| positive(stat.getVideoIncrement())
|| positive(stat.getLikeIncrement())
|| positive(stat.getCoinIncrement())
|| positive(stat.getFavoriteIncrement())
|| positive(stat.getViewIncrement()))
.map(UserStatDaily::getUserId)
.distinct()
.count();
stats.put("activeUsers", activeUsers);
// 用户等级分布
// 假设level字段代表用户等级
@@ -384,59 +385,61 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
*/
private void updateUserStatDaily(Long userId, String statType, int count) {
LocalDate today = LocalDate.now();
int followerIncrement = 0;
int followingIncrement = 0;
int videoIncrement = 0;
int likeIncrement = 0;
int coinIncrement = 0;
int favoriteIncrement = 0;
long viewIncrement = 0L;
// 查询今日统计记录
UserStatDaily statDaily = userStatDailyMapper.selectOne(
new LambdaQueryWrapper<UserStatDaily>()
.eq(UserStatDaily::getUserId, userId)
.eq(UserStatDaily::getStatDate, today)
);
// 如果不存在,创建新记录
if (statDaily == null) {
statDaily = new UserStatDaily();
statDaily.setUserId(userId);
statDaily.setStatDate(today);
statDaily.setFollowerIncrement(0);
statDaily.setFollowingIncrement(0);
statDaily.setVideoIncrement(0);
statDaily.setLikeIncrement(0);
statDaily.setCoinIncrement(0);
statDaily.setFavoriteIncrement(0);
statDaily.setViewIncrement(0L);
statDaily.setCreateTime(LocalDateTime.now());
}
// 根据统计类型更新数据
switch (statType) {
case "video":
statDaily.setVideoIncrement(statDaily.getVideoIncrement() + count);
videoIncrement = count;
break;
case "like":
statDaily.setLikeIncrement(statDaily.getLikeIncrement() + count);
likeIncrement = count;
break;
case "follower":
statDaily.setFollowerIncrement(statDaily.getFollowerIncrement() + count);
followerIncrement = count;
break;
case "following":
statDaily.setFollowingIncrement(statDaily.getFollowingIncrement() + count);
followingIncrement = count;
break;
case "favorite":
statDaily.setFavoriteIncrement(statDaily.getFavoriteIncrement() + count);
favoriteIncrement = count;
break;
case "coin":
statDaily.setCoinIncrement(statDaily.getCoinIncrement() + count);
coinIncrement = count;
break;
case "view":
statDaily.setViewIncrement(statDaily.getViewIncrement() + count);
viewIncrement = count;
break;
}
// 保存更新
if (statDaily.getId() == null) {
userStatDailyMapper.insert(statDaily);
} else {
userStatDailyMapper.updateById(statDaily);
}
userStatDailyMapper.upsertStatIncrement(
userId,
today,
followerIncrement,
followingIncrement,
videoIncrement,
likeIncrement,
coinIncrement,
favoriteIncrement,
viewIncrement
);
}
}
private void updateUserStatDailyAsync(Long userId, String statType, int count) {
CompletableFuture.runAsync(() -> {
try {
updateUserStatDaily(userId, statType, count);
} catch (Exception e) {
log.warn("异步更新用户日统计失败,userId={}, statType={}, count={}", userId, statType, count, e);
}
});
}
private boolean positive(Number value) {
return value != null && value.longValue() > 0;
}
}
@@ -18,13 +18,13 @@ spring:
datasource:
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/video_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
password: ${MYSQL_ROOT_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
password: redis_EsJ8FX
password: ${REDIS_PASSWORD}
database: 1
timeout: 10000
lettuce:
@@ -40,10 +40,25 @@ mybatis-plus:
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: is_deleted
logic-delete-value: 1
logic-not-delete-value: 0
logic-not-delete-value: 0
minio:
endpoint: http://${MINIO_HOST:minio}:${MINIO_PORT:9000}
accessKey: ${MINIO_ACCESS_KEY}
secretKey: ${MINIO_SECRET_KEY}
bucketName: ${MINIO_BUCKET}
service:
auth:
token: ${SERVICE_AUTH_TOKEN}
jwt:
secret: ${USER_JWT_SECRET}
expiration: ${JWT_EXPIRATION}
token-header: Authorization
token-prefix: Bearer
@@ -16,4 +16,25 @@
WHERE id = #{userId}
</update>
</mapper>
<update id="increaseVideoCount">
UPDATE user_info
SET video_count = GREATEST(video_count + #{count}, 0),
update_time = NOW()
WHERE id = #{userId}
</update>
<update id="increaseLikeCount">
UPDATE user_info
SET like_count = GREATEST(like_count + #{count}, 0),
update_time = NOW()
WHERE id = #{userId}
</update>
<update id="increaseFavoriteCount">
UPDATE user_info
SET favorite_count = GREATEST(favorite_count + #{count}, 0),
update_time = NOW()
WHERE id = #{userId}
</update>
</mapper>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.user.mapper.UserStatDailyMapper">
<insert id="upsertStatIncrement">
INSERT INTO user_stat_daily (
user_id,
stat_date,
follower_increment,
following_increment,
video_increment,
like_increment,
coin_increment,
favorite_increment,
view_increment
) VALUES (
#{userId},
#{statDate},
#{followerIncrement},
#{followingIncrement},
#{videoIncrement},
#{likeIncrement},
#{coinIncrement},
#{favoriteIncrement},
#{viewIncrement}
)
ON DUPLICATE KEY UPDATE
follower_increment = follower_increment + VALUES(follower_increment),
following_increment = following_increment + VALUES(following_increment),
video_increment = video_increment + VALUES(video_increment),
like_increment = like_increment + VALUES(like_increment),
coin_increment = coin_increment + VALUES(coin_increment),
favorite_increment = favorite_increment + VALUES(favorite_increment),
view_increment = view_increment + VALUES(view_increment)
</insert>
</mapper>
@@ -13,7 +13,9 @@ 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.MockitoSettings;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -27,6 +29,7 @@ import static org.mockito.Mockito.*;
* UserFollowService 单元测试
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@DisplayName("用户关注服务测试")
class UserFollowServiceTest {
@@ -1,30 +1,40 @@
package com.nuaa.video.user.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.common.utils.MinioUtils;
import com.nuaa.video.user.dto.UserProfileDTO;
import com.nuaa.video.user.entity.UserInfo;
import com.nuaa.video.user.entity.UserStatDaily;
import com.nuaa.video.user.feign.AuthFeignClient;
import com.nuaa.video.user.mapper.UserFollowMapper;
import com.nuaa.video.user.mapper.UserInfoMapper;
import com.nuaa.video.user.mapper.UserStatDailyMapper;
import com.nuaa.video.user.service.impl.UserInfoServiceImpl;
import com.nuaa.video.user.vo.UserProfileVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* UserInfoService 单元测试
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("用户信息服务测试")
class UserInfoServiceTest {
@@ -32,6 +42,22 @@ class UserInfoServiceTest {
@Mock
private UserInfoMapper userInfoMapper;
@Mock
private UserFollowMapper userFollowMapper;
@Mock
private UserSettingService userSettingService;
@Mock
private MinioUtils minioUtils;
@Mock
private UserStatDailyMapper userStatDailyMapper;
@Mock
private AuthFeignClient authFeignClient;
@Spy
@InjectMocks
private UserInfoServiceImpl userInfoService;
@@ -62,158 +88,127 @@ class UserInfoServiceTest {
@Test
@DisplayName("创建用户信息 - 成功")
void testCreateUserInfoSuccess() {
// Given
Long userId = 1L;
String username = "testuser";
doReturn(null).when(userInfoService).getById(1L);
doReturn(true).when(userInfoService).save(any(UserInfo.class));
when(userSettingService.createDefaultSetting(1L)).thenReturn(Result.success());
when(userInfoMapper.selectById(userId)).thenReturn(null);
when(userInfoMapper.insert(any(UserInfo.class))).thenReturn(1);
Result<Void> result = userInfoService.createUserInfo(1L, "testuser");
// When
Result<Void> result = userInfoService.createUserInfo(userId, username);
// Then
assertNotNull(result);
assertEquals(200, result.getCode());
verify(userInfoMapper, times(1)).insert(any(UserInfo.class));
verify(userInfoService, times(1)).save(ArgumentMatchers.argThat(user ->
user.getId().equals(1L)
&& "testuser".equals(user.getNickname())
&& "default/avatar.png".equals(user.getAvatar())
&& user.getVideoCount() == 0
));
verify(userSettingService, times(1)).createDefaultSetting(1L);
}
@Test
@DisplayName("创建用户信息 - 用户已存在")
@DisplayName("创建用户信息 - 已存在")
void testCreateUserInfoAlreadyExists() {
// Given
Long userId = 1L;
String username = "testuser";
doReturn(testUserInfo).when(userInfoService).getById(1L);
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
Result<Void> result = userInfoService.createUserInfo(1L, "testuser");
// When
Result<Void> result = userInfoService.createUserInfo(userId, username);
// Then
assertNotNull(result);
assertEquals(200, result.getCode());
verify(userInfoMapper, never()).insert(any(UserInfo.class));
assertNotEquals(200, result.getCode());
assertEquals("用户信息已存在", result.getMessage());
verify(userInfoService, times(0)).save(any(UserInfo.class));
}
@Test
@DisplayName("获取用户资料 - 成功")
@DisplayName("获取用户资料 - 成功并标记关注状态")
void testGetUserProfileSuccess() {
// Given
Long userId = 1L;
Long currentUserId = 2L;
doReturn(testUserInfo).when(userInfoService).getById(1L);
when(userFollowMapper.checkIsFollowed(2L, 1L)).thenReturn(1);
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
Result<UserProfileVO> result = userInfoService.getUserProfile(1L, 2L);
// Then
assertNotNull(testUserInfo);
assertEquals("测试用户", testUserInfo.getNickname());
}
@Test
@DisplayName("获取用户资料 - 用户不存在")
void testGetUserProfileNotFound() {
// Given
Long userId = 999L;
when(userInfoMapper.selectById(userId)).thenReturn(null);
// When
UserInfo user = userInfoMapper.selectById(userId);
// Then
assertNull(user);
assertEquals(200, result.getCode());
assertNotNull(result.getData());
assertEquals("测试用户", result.getData().getNickname());
assertTrue(result.getData().getIsFollowed());
}
@Test
@DisplayName("更新用户资料 - 成功")
void testUpdateUserProfileSuccess() {
// Given
Long userId = 1L;
UserProfileDTO dto = new UserProfileDTO();
dto.setNickname("新昵称");
dto.setSignature("新签名");
dto.setGender(1);
dto.setRegion("上海");
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
when(userInfoMapper.updateById(any(UserInfo.class))).thenReturn(1);
doReturn(testUserInfo).when(userInfoService).getById(1L);
doReturn(true).when(userInfoService).updateById(any(UserInfo.class));
// Then
assertNotNull(dto);
assertEquals("新昵称", dto.getNickname());
Result<Void> result = userInfoService.updateUserProfile(1L, dto);
assertEquals(200, result.getCode());
verify(userInfoService, times(1)).updateById(ArgumentMatchers.argThat(user ->
"新昵称".equals(user.getNickname())
&& "新签名".equals(user.getSignature())
&& "上海".equals(user.getRegion())
&& "1".equals(user.getUpdatedBy())
));
}
@Test
@DisplayName("获取简要用户信息 - 成功")
void testGetBriefUserInfoSuccess() {
// Given
Long userId = 1L;
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
// When
UserInfo user = userInfoMapper.selectById(userId);
// Then
assertNotNull(user);
assertEquals("测试用户", user.getNickname());
}
@Test
@DisplayName("搜索用户 - 成功")
void testSearchUsersSuccess() {
// Given
Page<UserInfo> page = new Page<>(1, 10);
String keyword = "测试";
// Then
assertNotNull(page);
assertEquals(1, page.getCurrent());
assertEquals(10, page.getSize());
}
@Test
@DisplayName("增加用户视频数量 - 成功")
@DisplayName("增加用户视频数量 - 成功并写入每日统计")
void testIncreaseVideoCountSuccess() {
// Given
Long userId = 1L;
Integer count = 1;
int oldVideoCount = testUserInfo.getVideoCount();
doReturn(testUserInfo).when(userInfoService).getById(1L);
doReturn(true).when(userInfoService).updateById(any(UserInfo.class));
when(userStatDailyMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(userStatDailyMapper.insert(ArgumentMatchers.<UserStatDaily>any())).thenReturn(1);
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
when(userInfoMapper.updateById(any(UserInfo.class))).thenReturn(1);
Result<Void> result = userInfoService.increaseVideoCount(1L, 2);
// Then
assertEquals(10, oldVideoCount);
assertEquals(200, result.getCode());
verify(userInfoService).updateById(ArgumentMatchers.argThat(user -> user.getVideoCount() == 12));
verify(userStatDailyMapper).insert(ArgumentMatchers.<UserStatDaily>argThat(stat ->
stat.getUserId().equals(1L)
&& stat.getVideoIncrement() == 2
&& stat.getStatDate().equals(LocalDate.now())
));
}
@Test
@DisplayName("增加用户获赞数量 - 成功")
void testIncreaseLikeCountSuccess() {
// Given
Long userId = 1L;
Integer count = 10;
int oldLikeCount = testUserInfo.getLikeCount();
@DisplayName("获取用户统计 - 基于近7日活跃记录聚合")
void testGetUserStatsSuccess() {
UserStatDaily activeOne = new UserStatDaily();
activeOne.setUserId(1L);
activeOne.setStatDate(LocalDate.now());
activeOne.setVideoIncrement(1);
activeOne.setViewIncrement(0L);
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
when(userInfoMapper.updateById(any(UserInfo.class))).thenReturn(1);
UserStatDaily activeTwo = new UserStatDaily();
activeTwo.setUserId(2L);
activeTwo.setStatDate(LocalDate.now().minusDays(1));
activeTwo.setLikeIncrement(3);
// Then
assertEquals(1000, oldLikeCount);
}
UserStatDaily inactive = new UserStatDaily();
inactive.setUserId(3L);
inactive.setStatDate(LocalDate.now().minusDays(2));
inactive.setVideoIncrement(0);
inactive.setLikeIncrement(0);
inactive.setFavoriteIncrement(0);
inactive.setCoinIncrement(0);
inactive.setFollowerIncrement(0);
inactive.setFollowingIncrement(0);
inactive.setViewIncrement(0L);
@Test
@DisplayName("增加用户收藏数量 - 成功")
void testIncreaseFavoriteCountSuccess() {
// Given
Long userId = 1L;
Integer count = 5;
int oldFavoriteCount = testUserInfo.getFavoriteCount();
doReturn(10L).when(userInfoService).count();
doReturn(2L).when(userInfoService).count(any(LambdaQueryWrapper.class));
when(userStatDailyMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(activeOne, activeTwo, inactive));
when(userInfoMapper.selectById(userId)).thenReturn(testUserInfo);
when(userInfoMapper.updateById(any(UserInfo.class))).thenReturn(1);
Result<Object> result = userInfoService.getUserStats();
// Then
assertEquals(30, oldFavoriteCount);
assertEquals(200, result.getCode());
assertNotNull(result.getData());
@SuppressWarnings("unchecked")
var stats = (java.util.Map<String, Object>) result.getData();
assertEquals(10L, stats.get("totalUsers"));
assertEquals(2L, stats.get("todayNewUsers"));
assertEquals(2L, stats.get("activeUsers"));
}
}